@chrrxs/robloxstudio-mcp-inspector 2.22.3 → 2.22.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3073,7 +3073,8 @@ var init_roblox_cookie_client = __esm({
3073
3073
  });
3074
3074
 
3075
3075
  // ../core/dist/managed-instance-registry.js
3076
- import * as fs from "fs";
3076
+ import * as fs from "fs/promises";
3077
+ import { randomUUID } from "crypto";
3077
3078
  import * as os from "os";
3078
3079
  import * as path from "path";
3079
3080
  function defaultManagedInstanceRegistryDir() {
@@ -3088,8 +3089,8 @@ function defaultManagedInstanceRegistryDir() {
3088
3089
  }
3089
3090
  return path.join(os.homedir(), ".local", "state", "robloxstudio-mcp", "managed-instances", "v1");
3090
3091
  }
3091
- function sleepSync(ms) {
3092
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
3092
+ function delay(ms) {
3093
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
3093
3094
  }
3094
3095
  function ymd(timestamp) {
3095
3096
  return new Date(timestamp).toISOString().slice(0, 10);
@@ -3104,92 +3105,131 @@ function isRecord(value) {
3104
3105
  const record = value;
3105
3106
  return record.version === REGISTRY_VERSION && typeof record.recordId === "string" && typeof record.source === "string" && typeof record.exe === "string" && Array.isArray(record.args) && typeof record.launchedAt === "number" && typeof record.bootId === "string";
3106
3107
  }
3107
- var REGISTRY_VERSION, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, EVENT_RETENTION_DAYS, TERMINAL_RECORD_RETENTION_MS, ManagedInstanceRegistry;
3108
+ function isLockOwner(value) {
3109
+ if (!value || typeof value !== "object")
3110
+ return false;
3111
+ const owner = value;
3112
+ return Number.isInteger(owner.pid) && owner.pid > 0 && typeof owner.token === "string" && owner.token.length > 0 && typeof owner.createdAt === "number";
3113
+ }
3114
+ function isProcessAlive(pid) {
3115
+ if (pid === process.pid)
3116
+ return true;
3117
+ try {
3118
+ process.kill(pid, 0);
3119
+ return true;
3120
+ } catch (error) {
3121
+ return error.code === "EPERM";
3122
+ }
3123
+ }
3124
+ var REGISTRY_VERSION, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, EVENT_RETENTION_DAYS, TERMINAL_RECORD_RETENTION_MS, DEFAULT_CONFIRMED_EXIT_MISSES, DEFAULT_CONFIRMED_EXIT_GRACE_MS, activeLockTokens, ManagedInstanceRegistry;
3108
3125
  var init_managed_instance_registry = __esm({
3109
3126
  "../core/dist/managed-instance-registry.js"() {
3110
3127
  "use strict";
3111
3128
  REGISTRY_VERSION = 1;
3112
3129
  LOCK_STALE_MS = 1e4;
3113
3130
  LOCK_RETRY_MS = 25;
3114
- LOCK_TIMEOUT_MS = 5e3;
3131
+ LOCK_TIMEOUT_MS = 15e3;
3115
3132
  EVENT_RETENTION_DAYS = 2;
3116
3133
  TERMINAL_RECORD_RETENTION_MS = 24 * 60 * 60 * 1e3;
3134
+ DEFAULT_CONFIRMED_EXIT_MISSES = 2;
3135
+ DEFAULT_CONFIRMED_EXIT_GRACE_MS = 5e3;
3136
+ activeLockTokens = /* @__PURE__ */ new Set();
3117
3137
  ManagedInstanceRegistry = class {
3118
3138
  dir;
3119
3139
  constructor(dir = defaultManagedInstanceRegistryDir()) {
3120
3140
  this.dir = dir;
3121
3141
  }
3122
- upsert(record) {
3123
- this.withLock(() => this.writeRecordUnlocked(record));
3142
+ async upsert(record) {
3143
+ await this.withLock(() => this.writeRecordUnlocked(record));
3124
3144
  }
3125
- attachInstanceId(recordId, instanceId) {
3126
- this.withLock(() => {
3127
- const record = this.readRecordUnlocked(recordId);
3145
+ async attachInstanceId(recordId, instanceId) {
3146
+ await this.withLock(async () => {
3147
+ const record = await this.readRecordUnlocked(recordId);
3128
3148
  if (!record)
3129
3149
  return;
3130
3150
  record.instanceId = instanceId;
3131
3151
  record.attachedAt = Date.now();
3132
- this.writeRecordUnlocked(record);
3152
+ await this.writeRecordUnlocked(record);
3133
3153
  });
3134
3154
  }
3135
- findOpenByInstanceId(instanceId, options) {
3136
- return this.withLock(() => {
3137
- this.sweepUnlocked(options);
3138
- return this.readOpenRecordsUnlocked().find((record) => record.instanceId === instanceId);
3155
+ async findOpenByInstanceId(instanceId, options) {
3156
+ return this.withLock(async () => {
3157
+ await this.sweepUnlocked(options);
3158
+ return (await this.readOpenRecordsUnlocked()).find((record) => record.instanceId === instanceId);
3139
3159
  });
3140
3160
  }
3141
- findAnyByInstanceId(instanceId) {
3142
- return this.withLock(() => this.readRecordsUnlocked().find((record) => record.instanceId === instanceId));
3161
+ async findAnyByInstanceId(instanceId) {
3162
+ return this.withLock(async () => (await this.readRecordsUnlocked()).find((record) => record.instanceId === instanceId));
3143
3163
  }
3144
- findAnyByRecordId(recordId, options) {
3145
- return this.withLock(() => {
3146
- this.sweepUnlocked(options);
3147
- return this.readRecordsUnlocked().find((record) => record.recordId === recordId);
3164
+ async findAnyByRecordId(recordId, options) {
3165
+ return this.withLock(async () => {
3166
+ if (options)
3167
+ await this.sweepUnlocked(options);
3168
+ return (await this.readRecordsUnlocked()).find((record) => record.recordId === recordId);
3148
3169
  });
3149
3170
  }
3150
- listOpen(options) {
3151
- return this.withLock(() => {
3152
- this.sweepUnlocked(options);
3171
+ async listOpen(options) {
3172
+ return this.withLock(async () => {
3173
+ await this.sweepUnlocked(options);
3153
3174
  return this.readOpenRecordsUnlocked();
3154
3175
  });
3155
3176
  }
3156
- listOpenUnchecked() {
3177
+ async listOpenUnchecked() {
3157
3178
  return this.withLock(() => this.readOpenRecordsUnlocked());
3158
3179
  }
3159
- markClosed(recordId, closedAt = Date.now()) {
3160
- this.withLock(() => {
3161
- const record = this.readRecordUnlocked(recordId);
3180
+ async markClosed(recordId, closedAt = Date.now()) {
3181
+ await this.withLock(async () => {
3182
+ const record = await this.readRecordUnlocked(recordId);
3162
3183
  if (!record)
3163
3184
  return;
3164
3185
  record.closedAt = closedAt;
3165
- this.writeRecordUnlocked(record);
3186
+ await this.writeRecordUnlocked(record);
3166
3187
  });
3167
3188
  }
3168
- delete(recordId) {
3169
- this.withLock(() => this.deleteRecordUnlocked(recordId));
3189
+ async delete(recordId) {
3190
+ await this.withLock(() => this.deleteRecordUnlocked(recordId));
3170
3191
  }
3171
- sweep(options) {
3172
- this.withLock(() => this.sweepUnlocked(options));
3192
+ async sweep(options) {
3193
+ await this.withLock(() => this.sweepUnlocked(options));
3173
3194
  }
3174
- logEvent(event, now = Date.now()) {
3175
- this.withLock(() => this.appendEventUnlocked(event, now));
3195
+ async logEvent(event, now = Date.now()) {
3196
+ await this.withLock(() => this.appendEventUnlocked(event, now));
3176
3197
  }
3177
- withLock(fn) {
3178
- this.ensureDir();
3198
+ async withLock(fn) {
3199
+ await this.ensureDir();
3179
3200
  const lockDir = path.join(this.dir, ".lock");
3180
3201
  const deadline = Date.now() + LOCK_TIMEOUT_MS;
3181
- while (true) {
3202
+ const owner = {
3203
+ pid: process.pid,
3204
+ token: randomUUID(),
3205
+ createdAt: Date.now()
3206
+ };
3207
+ for (; ; ) {
3182
3208
  try {
3183
- fs.mkdirSync(lockDir);
3209
+ await fs.mkdir(lockDir);
3210
+ activeLockTokens.add(owner.token);
3211
+ try {
3212
+ await fs.writeFile(path.join(lockDir, "owner.json"), `${JSON.stringify(owner)}
3213
+ `, {
3214
+ encoding: "utf8",
3215
+ flag: "wx"
3216
+ });
3217
+ } catch (error) {
3218
+ activeLockTokens.delete(owner.token);
3219
+ await fs.rm(lockDir, { recursive: true, force: true });
3220
+ throw error;
3221
+ }
3184
3222
  break;
3185
3223
  } catch (error) {
3186
3224
  const code = error.code;
3187
3225
  if (code !== "EEXIST")
3188
3226
  throw error;
3189
3227
  try {
3190
- const stat = fs.statSync(lockDir);
3191
- if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
3192
- fs.rmSync(lockDir, { recursive: true, force: true });
3228
+ const stat2 = await fs.stat(lockDir);
3229
+ const currentOwner = await this.readLockOwner(lockDir, stat2.isDirectory());
3230
+ const ownerIsActive = currentOwner?.pid === process.pid ? activeLockTokens.has(currentOwner.token) : currentOwner ? isProcessAlive(currentOwner.pid) : void 0;
3231
+ if (ownerIsActive === false || currentOwner === void 0 && Date.now() - stat2.mtimeMs > LOCK_STALE_MS) {
3232
+ await fs.rm(lockDir, { recursive: true, force: true });
3193
3233
  continue;
3194
3234
  }
3195
3235
  } catch {
@@ -3198,40 +3238,58 @@ var init_managed_instance_registry = __esm({
3198
3238
  if (Date.now() > deadline) {
3199
3239
  throw new Error(`Timed out waiting for managed instance registry lock: ${lockDir}`);
3200
3240
  }
3201
- sleepSync(LOCK_RETRY_MS);
3241
+ await delay(LOCK_RETRY_MS);
3202
3242
  }
3203
3243
  }
3204
3244
  try {
3205
- return fn();
3245
+ return await fn();
3206
3246
  } finally {
3207
- fs.rmSync(lockDir, { recursive: true, force: true });
3247
+ try {
3248
+ const stat2 = await fs.stat(lockDir);
3249
+ const currentOwner = await this.readLockOwner(lockDir, stat2.isDirectory());
3250
+ if (currentOwner?.pid === owner.pid && currentOwner.token === owner.token) {
3251
+ await fs.rm(lockDir, { recursive: true, force: true });
3252
+ }
3253
+ } catch {
3254
+ } finally {
3255
+ activeLockTokens.delete(owner.token);
3256
+ }
3257
+ }
3258
+ }
3259
+ async readLockOwner(lockPath, isDirectory) {
3260
+ try {
3261
+ const contents = await fs.readFile(isDirectory ? path.join(lockPath, "owner.json") : lockPath, "utf8");
3262
+ const parsed = JSON.parse(contents);
3263
+ return isLockOwner(parsed) ? parsed : void 0;
3264
+ } catch {
3265
+ return void 0;
3208
3266
  }
3209
3267
  }
3210
- ensureDir() {
3211
- fs.mkdirSync(this.dir, { recursive: true });
3268
+ async ensureDir() {
3269
+ await fs.mkdir(this.dir, { recursive: true });
3212
3270
  }
3213
3271
  recordPath(recordId) {
3214
3272
  return path.join(this.dir, `${recordId}.json`);
3215
3273
  }
3216
- recordFilesUnlocked() {
3217
- return fs.readdirSync(this.dir).filter((name) => name.endsWith(".json")).map((name) => path.join(this.dir, name));
3274
+ async recordFilesUnlocked() {
3275
+ return (await fs.readdir(this.dir)).filter((name) => name.endsWith(".json")).map((name) => path.join(this.dir, name));
3218
3276
  }
3219
- readRecordUnlocked(recordId) {
3277
+ async readRecordUnlocked(recordId) {
3220
3278
  try {
3221
- const parsed = JSON.parse(fs.readFileSync(this.recordPath(recordId), "utf8"));
3279
+ const parsed = JSON.parse(await fs.readFile(this.recordPath(recordId), "utf8"));
3222
3280
  return isRecord(parsed) ? parsed : void 0;
3223
3281
  } catch {
3224
3282
  return void 0;
3225
3283
  }
3226
3284
  }
3227
- readOpenRecordsUnlocked() {
3228
- return this.readRecordsUnlocked().filter((record) => record.closedAt === void 0);
3285
+ async readOpenRecordsUnlocked() {
3286
+ return (await this.readRecordsUnlocked()).filter((record) => record.closedAt === void 0);
3229
3287
  }
3230
- readRecordsUnlocked() {
3288
+ async readRecordsUnlocked() {
3231
3289
  const records = [];
3232
- for (const file of this.recordFilesUnlocked()) {
3290
+ for (const file of await this.recordFilesUnlocked()) {
3233
3291
  try {
3234
- const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
3292
+ const parsed = JSON.parse(await fs.readFile(file, "utf8"));
3235
3293
  if (!isRecord(parsed))
3236
3294
  continue;
3237
3295
  records.push(parsed);
@@ -3240,48 +3298,48 @@ var init_managed_instance_registry = __esm({
3240
3298
  }
3241
3299
  return records;
3242
3300
  }
3243
- writeRecordUnlocked(record) {
3244
- this.ensureDir();
3301
+ async writeRecordUnlocked(record) {
3302
+ await this.ensureDir();
3245
3303
  const finalPath = this.recordPath(record.recordId);
3246
3304
  const tmpPath = path.join(this.dir, `${record.recordId}.${process.pid}.${Date.now()}.tmp`);
3247
- const fd = fs.openSync(tmpPath, "w");
3305
+ const fd = await fs.open(tmpPath, "w");
3248
3306
  try {
3249
- fs.writeFileSync(fd, `${JSON.stringify(record, null, 2)}
3307
+ await fd.writeFile(`${JSON.stringify(record, null, 2)}
3250
3308
  `, "utf8");
3251
- fs.fsyncSync(fd);
3309
+ await fd.sync();
3252
3310
  } finally {
3253
- fs.closeSync(fd);
3311
+ await fd.close();
3254
3312
  }
3255
- fs.renameSync(tmpPath, finalPath);
3313
+ await fs.rename(tmpPath, finalPath);
3256
3314
  }
3257
- deleteRecordUnlocked(recordId) {
3258
- fs.rmSync(this.recordPath(recordId), { force: true });
3315
+ async deleteRecordUnlocked(recordId) {
3316
+ await fs.rm(this.recordPath(recordId), { force: true });
3259
3317
  }
3260
- appendEventUnlocked(event, now) {
3318
+ async appendEventUnlocked(event, now) {
3261
3319
  const file = path.join(this.dir, `events-${ymd(now)}.jsonl`);
3262
- fs.appendFileSync(file, `${JSON.stringify({
3320
+ await fs.appendFile(file, `${JSON.stringify({
3263
3321
  ts: new Date(now).toISOString(),
3264
3322
  ...event
3265
3323
  })}
3266
3324
  `, "utf8");
3267
3325
  }
3268
- cleanupOldEventLogsUnlocked(now) {
3326
+ async cleanupOldEventLogsUnlocked(now) {
3269
3327
  const cutoff = ymd(now - EVENT_RETENTION_DAYS * 24 * 60 * 60 * 1e3);
3270
- for (const name of fs.readdirSync(this.dir)) {
3328
+ for (const name of await fs.readdir(this.dir)) {
3271
3329
  const date = eventLogDate(name);
3272
3330
  if (!date || date >= cutoff)
3273
3331
  continue;
3274
3332
  try {
3275
- fs.rmSync(path.join(this.dir, name), { force: true });
3333
+ await fs.rm(path.join(this.dir, name), { force: true });
3276
3334
  } catch {
3277
3335
  }
3278
3336
  }
3279
3337
  }
3280
- cleanupRecord(options, record) {
3338
+ async cleanupRecord(options, record) {
3281
3339
  try {
3282
- options.cleanupRecord?.(record);
3340
+ await options.cleanupRecord?.(record);
3283
3341
  } catch {
3284
- this.appendEventUnlocked({
3342
+ await this.appendEventUnlocked({
3285
3343
  event: "registry_cleanup_failed",
3286
3344
  recordId: record.recordId,
3287
3345
  instanceId: record.instanceId,
@@ -3290,16 +3348,16 @@ var init_managed_instance_registry = __esm({
3290
3348
  }, options.now ?? Date.now());
3291
3349
  }
3292
3350
  }
3293
- sweepUnlocked(options) {
3351
+ async sweepUnlocked(options) {
3294
3352
  const now = options.now ?? Date.now();
3295
- this.cleanupOldEventLogsUnlocked(now);
3296
- for (const file of this.recordFilesUnlocked()) {
3353
+ await this.cleanupOldEventLogsUnlocked(now);
3354
+ for (const file of await this.recordFilesUnlocked()) {
3297
3355
  let parsed;
3298
3356
  try {
3299
- parsed = JSON.parse(fs.readFileSync(file, "utf8"));
3357
+ parsed = JSON.parse(await fs.readFile(file, "utf8"));
3300
3358
  } catch {
3301
- fs.rmSync(file, { force: true });
3302
- this.appendEventUnlocked({
3359
+ await fs.rm(file, { force: true });
3360
+ await this.appendEventUnlocked({
3303
3361
  event: "registry_pruned_malformed_record",
3304
3362
  reason: "parse_error",
3305
3363
  action: "deleted_record"
@@ -3307,8 +3365,8 @@ var init_managed_instance_registry = __esm({
3307
3365
  continue;
3308
3366
  }
3309
3367
  if (!parsed || typeof parsed !== "object") {
3310
- fs.rmSync(file, { force: true });
3311
- this.appendEventUnlocked({
3368
+ await fs.rm(file, { force: true });
3369
+ await this.appendEventUnlocked({
3312
3370
  event: "registry_pruned_malformed_record",
3313
3371
  reason: "invalid_shape",
3314
3372
  action: "deleted_record"
@@ -3319,8 +3377,8 @@ var init_managed_instance_registry = __esm({
3319
3377
  if (typeof version === "number" && version > REGISTRY_VERSION)
3320
3378
  continue;
3321
3379
  if (!isRecord(parsed)) {
3322
- fs.rmSync(file, { force: true });
3323
- this.appendEventUnlocked({
3380
+ await fs.rm(file, { force: true });
3381
+ await this.appendEventUnlocked({
3324
3382
  event: "registry_pruned_malformed_record",
3325
3383
  reason: "invalid_shape",
3326
3384
  action: "deleted_record"
@@ -3330,8 +3388,8 @@ var init_managed_instance_registry = __esm({
3330
3388
  const terminalAt = parsed.closedAt ?? parsed.exitedAt;
3331
3389
  if (terminalAt !== void 0) {
3332
3390
  if (now - terminalAt > TERMINAL_RECORD_RETENTION_MS) {
3333
- fs.rmSync(file, { force: true });
3334
- this.appendEventUnlocked({
3391
+ await fs.rm(file, { force: true });
3392
+ await this.appendEventUnlocked({
3335
3393
  event: "registry_pruned_terminal_record",
3336
3394
  recordId: parsed.recordId,
3337
3395
  instanceId: parsed.instanceId,
@@ -3343,13 +3401,13 @@ var init_managed_instance_registry = __esm({
3343
3401
  continue;
3344
3402
  }
3345
3403
  if (parsed.bootId !== options.currentBootId) {
3346
- this.cleanupRecord(options, parsed);
3404
+ await this.cleanupRecord(options, parsed);
3347
3405
  parsed.state = parsed.state === "failed" ? "failed" : "exited";
3348
3406
  parsed.exitedAt = now;
3349
3407
  parsed.closedAt = now;
3350
3408
  parsed.failureReason ??= "Studio process belongs to a previous host boot.";
3351
- this.writeRecordUnlocked(parsed);
3352
- this.appendEventUnlocked({
3409
+ await this.writeRecordUnlocked(parsed);
3410
+ await this.appendEventUnlocked({
3353
3411
  event: "registry_marked_previous_boot_exited",
3354
3412
  recordId: parsed.recordId,
3355
3413
  instanceId: parsed.instanceId,
@@ -3359,22 +3417,56 @@ var init_managed_instance_registry = __esm({
3359
3417
  }, now);
3360
3418
  continue;
3361
3419
  }
3362
- if (options.isProcessRunning && (parsed.nativeProcessId || parsed.spawnPid) && !options.isProcessRunning(parsed)) {
3363
- this.cleanupRecord(options, parsed);
3364
- parsed.state = parsed.state === "failed" ? "failed" : "exited";
3365
- parsed.exitedAt = now;
3366
- parsed.closedAt = now;
3367
- parsed.failureReason ??= parsed.instanceId ? "Studio process exited." : "Studio process exited before the MCP plugin connected.";
3368
- this.writeRecordUnlocked(parsed);
3369
- this.appendEventUnlocked({
3370
- event: "registry_marked_process_exited",
3371
- recordId: parsed.recordId,
3372
- instanceId: parsed.instanceId,
3373
- source: parsed.source,
3374
- reason: "pid_not_running",
3375
- action: "marked_exited_and_cleaned_baseplate"
3376
- }, now);
3420
+ if (!options.observeProcess || !(parsed.nativeProcessId || parsed.spawnPid))
3421
+ continue;
3422
+ const observation = await options.observeProcess(parsed);
3423
+ if (observation.status === "unknown") {
3424
+ parsed.processObservationStatus = "unknown";
3425
+ parsed.lastProcessObservationAt = observation.observedAt;
3426
+ parsed.lastProcessObservationError = observation.error;
3427
+ parsed.consecutiveConfirmedMisses = 0;
3428
+ parsed.firstConfirmedMissAt = void 0;
3429
+ await this.writeRecordUnlocked(parsed);
3430
+ continue;
3377
3431
  }
3432
+ const previousObservationAt = parsed.lastProcessObservationAt;
3433
+ parsed.lastSuccessfulProcessObservationAt = observation.observedAt;
3434
+ parsed.lastProcessObservationAt = observation.observedAt;
3435
+ parsed.lastProcessObservationError = void 0;
3436
+ if (observation.status === "running") {
3437
+ parsed.processObservationStatus = "running";
3438
+ parsed.consecutiveConfirmedMisses = 0;
3439
+ parsed.firstConfirmedMissAt = void 0;
3440
+ await this.writeRecordUnlocked(parsed);
3441
+ continue;
3442
+ }
3443
+ const isNewObservation = previousObservationAt !== observation.observedAt;
3444
+ parsed.processObservationStatus = "not_running";
3445
+ if (previousObservationAt !== observation.observedAt) {
3446
+ parsed.consecutiveConfirmedMisses = (parsed.consecutiveConfirmedMisses ?? 0) + 1;
3447
+ parsed.firstConfirmedMissAt ??= observation.observedAt;
3448
+ }
3449
+ const requiredMisses = options.confirmedExitMisses ?? DEFAULT_CONFIRMED_EXIT_MISSES;
3450
+ const graceMs = options.confirmedExitGraceMs ?? DEFAULT_CONFIRMED_EXIT_GRACE_MS;
3451
+ const confirmedAbsent = observation.reason === "identity_mismatch" || isNewObservation && (parsed.consecutiveConfirmedMisses ?? 0) >= requiredMisses && observation.observedAt - (parsed.firstConfirmedMissAt ?? observation.observedAt) >= graceMs;
3452
+ if (!confirmedAbsent) {
3453
+ await this.writeRecordUnlocked(parsed);
3454
+ continue;
3455
+ }
3456
+ await this.cleanupRecord(options, parsed);
3457
+ parsed.state = parsed.state === "failed" ? "failed" : "exited";
3458
+ parsed.exitedAt = observation.observedAt;
3459
+ parsed.closedAt = observation.observedAt;
3460
+ parsed.failureReason = observation.reason === "identity_mismatch" ? "Studio process identity changed; the retained PID was not reused." : parsed.instanceId ? "Studio process exited." : "Studio process exited before the MCP plugin connected.";
3461
+ await this.writeRecordUnlocked(parsed);
3462
+ await this.appendEventUnlocked({
3463
+ event: "registry_marked_process_exited",
3464
+ recordId: parsed.recordId,
3465
+ instanceId: parsed.instanceId,
3466
+ source: parsed.source,
3467
+ reason: observation.reason === "identity_mismatch" ? "identity_mismatch" : "pid_not_running",
3468
+ action: "marked_exited_and_cleaned_baseplate"
3469
+ }, now);
3378
3470
  }
3379
3471
  }
3380
3472
  };
@@ -3382,11 +3474,12 @@ var init_managed_instance_registry = __esm({
3382
3474
  });
3383
3475
 
3384
3476
  // ../core/dist/studio-instance-manager.js
3385
- import { execFileSync, spawn } from "child_process";
3386
- import { copyFileSync, existsSync as existsSync2, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync3, realpathSync, rmSync as rmSync2, statSync as statSync2 } from "fs";
3387
- import { randomUUID } from "crypto";
3477
+ import { execFile, execFileSync, spawn } from "child_process";
3478
+ import { copyFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync2, realpathSync, rmSync, statSync } from "fs";
3479
+ import { randomUUID as randomUUID2 } from "crypto";
3388
3480
  import * as os2 from "os";
3389
3481
  import * as path2 from "path";
3482
+ import { promisify } from "util";
3390
3483
  function run(command, args, options = {}) {
3391
3484
  return execFileSync(command, args, {
3392
3485
  encoding: "utf8",
@@ -3394,17 +3487,29 @@ function run(command, args, options = {}) {
3394
3487
  ...options
3395
3488
  }).trim();
3396
3489
  }
3490
+ async function runAsync(command, args, options = {}) {
3491
+ const result = await execFileAsync(command, args, {
3492
+ encoding: "utf8",
3493
+ maxBuffer: 4 * 1024 * 1024,
3494
+ timeout: 15e3,
3495
+ killSignal: "SIGKILL",
3496
+ ...options
3497
+ });
3498
+ return `${result.stdout}`.trim();
3499
+ }
3397
3500
  function isWsl() {
3398
3501
  if (process.platform !== "linux")
3399
3502
  return false;
3503
+ if (!process.env.WSL_INTEROP && !process.env.WSL_DISTRO_NAME)
3504
+ return false;
3400
3505
  try {
3401
- return /microsoft|wsl/i.test(readFileSync3("/proc/version", "utf8"));
3506
+ return /microsoft|wsl/i.test(readFileSync2("/proc/version", "utf8"));
3402
3507
  } catch {
3403
3508
  return false;
3404
3509
  }
3405
3510
  }
3406
- function powershell(script) {
3407
- return run("powershell.exe", ["-NoProfile", "-Command", script], {
3511
+ async function powershellAsync(script) {
3512
+ return runAsync("powershell.exe", ["-NoProfile", "-Command", script], {
3408
3513
  cwd: isWsl() && existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd()
3409
3514
  });
3410
3515
  }
@@ -3421,15 +3526,33 @@ function windowsLocalAppData() {
3421
3526
  return void 0;
3422
3527
  }
3423
3528
  }
3529
+ async function windowsLocalAppDataAsync() {
3530
+ if (process.platform === "win32")
3531
+ return process.env.LOCALAPPDATA;
3532
+ if (!isWsl())
3533
+ return void 0;
3534
+ try {
3535
+ return await runAsync("cmd.exe", ["/c", "echo %LOCALAPPDATA%"], {
3536
+ cwd: existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd()
3537
+ });
3538
+ } catch {
3539
+ return void 0;
3540
+ }
3541
+ }
3424
3542
  function toWslPath(windowsPath) {
3425
3543
  if (!isWsl())
3426
3544
  return windowsPath;
3427
3545
  return run("wslpath", ["-u", windowsPath]);
3428
3546
  }
3429
- function toStudioLaunchArg(arg) {
3547
+ async function toWslPathAsync(windowsPath) {
3548
+ if (!isWsl())
3549
+ return windowsPath;
3550
+ return runAsync("wslpath", ["-u", windowsPath]);
3551
+ }
3552
+ async function toStudioLaunchArgAsync(arg) {
3430
3553
  if (!isWsl() || !path2.isAbsolute(arg) || !existsSync2(arg))
3431
3554
  return arg;
3432
- return run("wslpath", ["-w", arg]);
3555
+ return runAsync("wslpath", ["-w", arg]);
3433
3556
  }
3434
3557
  function powershellStringLiteral(value) {
3435
3558
  return `'${value.replace(/'/g, "''")}'`;
@@ -3513,9 +3636,8 @@ function quoteWindowsCommandLineArg(value) {
3513
3636
  }
3514
3637
  return `${quoted}${"\\".repeat(backslashes * 2)}"`;
3515
3638
  }
3516
- function buildWindowsStudioStartScript(exe, args, processEnvironment) {
3639
+ function buildWindowsStudioStartScriptFromConvertedExe(windowsExe, args, processEnvironment) {
3517
3640
  const environmentPatch = parseStudioProcessEnvironmentPatch(processEnvironment);
3518
- const windowsExe = toStudioLaunchArg(exe);
3519
3641
  const commandLine = args.map(quoteWindowsCommandLineArg).join(" ");
3520
3642
  return [
3521
3643
  ...environmentPatch ? powershellEnvironmentPatchStatements(environmentPatch) : [],
@@ -3524,7 +3646,7 @@ function buildWindowsStudioStartScript(exe, args, processEnvironment) {
3524
3646
  `$psi.Arguments = ${powershellStringLiteral(commandLine)}`,
3525
3647
  // With UseShellExecute=false, Studio inherits the synchronous
3526
3648
  // powershell.exe invocation's stdout/stderr pipe handles under WSL. Those
3527
- // handles keep execFileSync waiting until Studio exits even though
3649
+ // handles keep the PowerShell invocation waiting until Studio exits even though
3528
3650
  // PowerShell already printed the PID. Shell execution prevents that
3529
3651
  // inheritance while Process.Start still returns the native Studio PID.
3530
3652
  "$psi.UseShellExecute = $true",
@@ -3532,9 +3654,10 @@ function buildWindowsStudioStartScript(exe, args, processEnvironment) {
3532
3654
  'if ($null -eq $studio) { throw "Roblox Studio process did not start." }'
3533
3655
  ].join("; ");
3534
3656
  }
3535
- function spawnWindowsStudioFromWsl(exe, args, processEnvironment) {
3536
- const script = buildWindowsStudioStartScript(exe, args, processEnvironment);
3537
- const output = powershell(`${script}; [PSCustomObject]@{ pid = $studio.Id; started = $studio.StartTime.ToUniversalTime().ToFileTimeUtc().ToString() } | ConvertTo-Json -Compress`);
3657
+ async function spawnWindowsStudioFromWsl(exe, args, processEnvironment) {
3658
+ const windowsExe = await toStudioLaunchArgAsync(exe);
3659
+ const script = buildWindowsStudioStartScriptFromConvertedExe(windowsExe, args, processEnvironment);
3660
+ const output = await powershellAsync(`${script}; [PSCustomObject]@{ pid = $studio.Id; started = $studio.StartTime.ToUniversalTime().ToFileTimeUtc().ToString() } | ConvertTo-Json -Compress`);
3538
3661
  const parsed = JSON.parse(output);
3539
3662
  const nativePid = Number(parsed.pid);
3540
3663
  const nativeStartedAt = typeof parsed.started === "string" && /^\d+$/u.test(parsed.started) ? parsed.started : void 0;
@@ -3576,7 +3699,7 @@ function resolveBaseplateTemplatePath() {
3576
3699
  }
3577
3700
  throw new Error(`Baseplate template not found. Expected ${BASEPLATE_TEMPLATE_NAME} in one of: ${candidates.join(", ")}`);
3578
3701
  }
3579
- function isProcessAlive(pid) {
3702
+ function isProcessAlive2(pid) {
3580
3703
  try {
3581
3704
  process.kill(pid, 0);
3582
3705
  return true;
@@ -3587,7 +3710,7 @@ function isProcessAlive(pid) {
3587
3710
  function sweepStaleBaseplateFiles() {
3588
3711
  let entries;
3589
3712
  try {
3590
- entries = readdirSync2(BASEPLATE_TEMP_DIR);
3713
+ entries = readdirSync(BASEPLATE_TEMP_DIR);
3591
3714
  } catch {
3592
3715
  return;
3593
3716
  }
@@ -3596,18 +3719,18 @@ function sweepStaleBaseplateFiles() {
3596
3719
  const match = BASEPLATE_TEMP_SWEEP_NAME.exec(entry);
3597
3720
  if (!match)
3598
3721
  continue;
3599
- if (Number(match[1]) !== process.pid && isProcessAlive(Number(match[1])))
3722
+ if (Number(match[1]) !== process.pid && isProcessAlive2(Number(match[1])))
3600
3723
  continue;
3601
3724
  const file = path2.join(BASEPLATE_TEMP_DIR, entry);
3602
3725
  try {
3603
- if (statSync2(file).mtimeMs < cutoff)
3604
- rmSync2(file, { force: true });
3726
+ if (statSync(file).mtimeMs < cutoff)
3727
+ rmSync(file, { force: true });
3605
3728
  } catch {
3606
3729
  }
3607
3730
  }
3608
3731
  }
3609
3732
  function createBaseplatePlaceFile() {
3610
- mkdirSync3(BASEPLATE_TEMP_DIR, { recursive: true });
3733
+ mkdirSync2(BASEPLATE_TEMP_DIR, { recursive: true });
3611
3734
  sweepStaleBaseplateFiles();
3612
3735
  const file = path2.join(BASEPLATE_TEMP_DIR, `Baseplate-${process.pid}-${Date.now()}.rbxl`);
3613
3736
  copyFileSync(resolveBaseplateTemplatePath(), file);
@@ -3624,7 +3747,7 @@ function cleanupManagedBaseplateFiles(record) {
3624
3747
  return;
3625
3748
  for (const file of [record.localPlaceFile, `${record.localPlaceFile}.lock`]) {
3626
3749
  try {
3627
- rmSync2(file, { force: true });
3750
+ rmSync(file, { force: true });
3628
3751
  } catch {
3629
3752
  }
3630
3753
  }
@@ -3651,54 +3774,83 @@ function resolveStudioExe() {
3651
3774
  if (!existsSync2(root)) {
3652
3775
  throw new Error(`Roblox Studio Versions folder not found: ${root}. Set ROBLOX_STUDIO_EXE.`);
3653
3776
  }
3654
- const candidates = readdirSync2(root).filter((name) => name.startsWith("version-")).map((name) => path2.join(root, name, "RobloxStudioBeta.exe")).filter((candidate) => existsSync2(candidate)).sort((a, b) => statSync2(b).mtimeMs - statSync2(a).mtimeMs);
3777
+ const candidates = readdirSync(root).filter((name) => name.startsWith("version-")).map((name) => path2.join(root, name, "RobloxStudioBeta.exe")).filter((candidate) => existsSync2(candidate)).sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs);
3655
3778
  if (candidates.length === 0) {
3656
3779
  throw new Error(`RobloxStudioBeta.exe not found under ${root}. Set ROBLOX_STUDIO_EXE.`);
3657
3780
  }
3658
3781
  return candidates[0];
3659
3782
  }
3660
- function listStudioProcesses() {
3783
+ async function resolveStudioExeAsync() {
3784
+ if (process.env.ROBLOX_STUDIO_EXE)
3785
+ return process.env.ROBLOX_STUDIO_EXE;
3661
3786
  if (process.platform === "darwin") {
3662
- let out2 = "";
3663
- try {
3664
- out2 = run("pgrep", ["-fl", "RobloxStudio"]);
3665
- } catch {
3666
- return [];
3667
- }
3668
- return out2.split("\n").filter(Boolean).map((line) => {
3669
- const [pid, ...rest] = line.trim().split(/\s+/);
3670
- return { Id: Number(pid), Name: "RobloxStudio", Path: rest.join(" "), MainWindowTitle: "" };
3671
- }).filter((proc) => Number.isFinite(proc.Id));
3787
+ return "/Applications/RobloxStudio.app/Contents/MacOS/RobloxStudio";
3672
3788
  }
3673
- if (process.platform !== "win32" && !isWsl())
3674
- return [];
3675
- let out = "";
3789
+ if (process.platform !== "win32" && !isWsl()) {
3790
+ throw new Error("Roblox Studio executable auto-discovery is only supported on Windows, WSL, and macOS. Set ROBLOX_STUDIO_EXE.");
3791
+ }
3792
+ const localAppData = await windowsLocalAppDataAsync();
3793
+ const root = localAppData ? path2.join(await toWslPathAsync(localAppData), "Roblox", "Versions") : path2.join(os2.homedir(), "AppData", "Local", "Roblox", "Versions");
3794
+ if (!existsSync2(root)) {
3795
+ throw new Error(`Roblox Studio Versions folder not found: ${root}. Set ROBLOX_STUDIO_EXE.`);
3796
+ }
3797
+ const candidates = readdirSync(root).filter((name) => name.startsWith("version-")).map((name) => path2.join(root, name, "RobloxStudioBeta.exe")).filter((candidate) => existsSync2(candidate)).sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs);
3798
+ if (candidates.length === 0) {
3799
+ throw new Error(`RobloxStudioBeta.exe not found under ${root}. Set ROBLOX_STUDIO_EXE.`);
3800
+ }
3801
+ return candidates[0];
3802
+ }
3803
+ async function observeStudioProcesses() {
3804
+ const observedAt = Date.now();
3676
3805
  try {
3677
- out = powershell("Get-Process RobloxStudioBeta -ErrorAction SilentlyContinue | ForEach-Object { [PSCustomObject]@{ Id = $_.Id; Name = $_.Name; Path = $_.Path; MainWindowTitle = $_.MainWindowTitle; StartTimeUtcFileTime = $_.StartTime.ToUniversalTime().ToFileTimeUtc().ToString() } } | ConvertTo-Json -Compress");
3678
- } catch {
3679
- return [];
3806
+ if (process.platform === "darwin") {
3807
+ let out2 = "";
3808
+ try {
3809
+ out2 = await runAsync("pgrep", ["-fl", "RobloxStudio"]);
3810
+ } catch (error) {
3811
+ const code = error.code;
3812
+ if (Number(code) === 1)
3813
+ return { status: "ok", observedAt, processes: [] };
3814
+ throw error;
3815
+ }
3816
+ const processes = out2.split("\n").filter(Boolean).map((line) => {
3817
+ const [pid, ...rest] = line.trim().split(/\s+/);
3818
+ return { Id: Number(pid), Name: "RobloxStudio", Path: rest.join(" "), MainWindowTitle: "" };
3819
+ }).filter((proc) => Number.isFinite(proc.Id));
3820
+ return { status: "ok", observedAt, processes };
3821
+ }
3822
+ if (process.platform !== "win32" && !isWsl()) {
3823
+ return { status: "ok", observedAt, processes: [] };
3824
+ }
3825
+ const out = await powershellAsync("Get-Process RobloxStudioBeta -ErrorAction SilentlyContinue | ForEach-Object { [PSCustomObject]@{ Id = $_.Id; Name = $_.Name; Path = $_.Path; MainWindowTitle = $_.MainWindowTitle; StartTimeUtcFileTime = $_.StartTime.ToUniversalTime().ToFileTimeUtc().ToString() } } | ConvertTo-Json -Compress");
3826
+ if (!out)
3827
+ return { status: "ok", observedAt, processes: [] };
3828
+ const parsed = JSON.parse(out);
3829
+ return { status: "ok", observedAt, processes: Array.isArray(parsed) ? parsed : [parsed] };
3830
+ } catch (error) {
3831
+ return {
3832
+ status: "error",
3833
+ observedAt,
3834
+ error: error instanceof Error ? error.message : String(error)
3835
+ };
3680
3836
  }
3681
- if (!out)
3682
- return [];
3683
- const parsed = JSON.parse(out);
3684
- return Array.isArray(parsed) ? parsed : [parsed];
3685
3837
  }
3686
- function currentBootId() {
3838
+ async function currentBootIdAsync() {
3687
3839
  if (process.platform === "linux") {
3688
3840
  try {
3689
- return readFileSync3("/proc/sys/kernel/random/boot_id", "utf8").trim();
3841
+ return readFileSync2("/proc/sys/kernel/random/boot_id", "utf8").trim();
3690
3842
  } catch {
3691
3843
  }
3692
3844
  }
3693
3845
  if (process.platform === "win32" || isWsl()) {
3694
3846
  try {
3695
- return powershell('(Get-CimInstance Win32_OperatingSystem).LastBootUpTime.ToUniversalTime().ToString("o")');
3847
+ return await powershellAsync('(Get-CimInstance Win32_OperatingSystem).LastBootUpTime.ToUniversalTime().ToString("o")');
3696
3848
  } catch {
3697
3849
  }
3698
3850
  }
3699
3851
  if (process.platform === "darwin") {
3700
3852
  try {
3701
- return run("sysctl", ["-n", "kern.boottime"]);
3853
+ return await runAsync("sysctl", ["-n", "kern.boottime"]);
3702
3854
  } catch {
3703
3855
  }
3704
3856
  }
@@ -3737,13 +3889,13 @@ function buildStudioLaunchArgs(options) {
3737
3889
  ];
3738
3890
  }
3739
3891
  }
3740
- function delay(ms) {
3892
+ function delay2(ms) {
3741
3893
  return new Promise((resolve5) => setTimeout(resolve5, ms));
3742
3894
  }
3743
3895
  function basenameAny(filePath) {
3744
3896
  return path2.basename(filePath.replace(/\\/g, "/"));
3745
3897
  }
3746
- var BASEPLATE_TEMP_DIR, BASEPLATE_TEMP_NAME, BASEPLATE_TEMPLATE_NAME, ENVIRONMENT_VARIABLE_NAME, STALE_BASEPLATE_MAX_AGE_MS, BASEPLATE_TEMP_SWEEP_NAME, StudioInstanceManager;
3898
+ var BASEPLATE_TEMP_DIR, BASEPLATE_TEMP_NAME, BASEPLATE_TEMPLATE_NAME, execFileAsync, ENVIRONMENT_VARIABLE_NAME, STALE_BASEPLATE_MAX_AGE_MS, BASEPLATE_TEMP_SWEEP_NAME, StudioInstanceManager;
3747
3899
  var init_studio_instance_manager = __esm({
3748
3900
  "../core/dist/studio-instance-manager.js"() {
3749
3901
  "use strict";
@@ -3751,27 +3903,39 @@ var init_studio_instance_manager = __esm({
3751
3903
  BASEPLATE_TEMP_DIR = path2.join(os2.tmpdir(), "robloxstudio-mcp-baseplates");
3752
3904
  BASEPLATE_TEMP_NAME = /^Baseplate-\d+-\d+\.rbxl$/;
3753
3905
  BASEPLATE_TEMPLATE_NAME = "Baseplate.rbxl";
3906
+ execFileAsync = promisify(execFile);
3754
3907
  ENVIRONMENT_VARIABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/u;
3755
3908
  STALE_BASEPLATE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
3756
3909
  BASEPLATE_TEMP_SWEEP_NAME = /^Baseplate-(\d+)-\d+\.rbxl(\.lock)?$/;
3757
3910
  StudioInstanceManager = class {
3758
3911
  managedByInstanceId = /* @__PURE__ */ new Map();
3759
3912
  pending = /* @__PURE__ */ new Set();
3760
- monitors = /* @__PURE__ */ new Map();
3761
3913
  connectionTimers = /* @__PURE__ */ new Map();
3762
3914
  registry;
3763
3915
  processAdapter;
3916
+ confirmedExitMisses;
3917
+ confirmedExitGraceMs;
3918
+ snapshotCacheMs;
3919
+ coordinatorTimer;
3920
+ coordinatorRefresh;
3921
+ cachedSnapshot;
3922
+ snapshotInFlight;
3923
+ launchQueue = Promise.resolve();
3764
3924
  constructor(options = {}) {
3765
3925
  this.registry = options.registry ?? new ManagedInstanceRegistry(options.registryDir);
3766
3926
  this.processAdapter = options.processAdapter ?? {};
3927
+ this.confirmedExitMisses = options.confirmedExitMisses ?? 2;
3928
+ this.confirmedExitGraceMs = options.confirmedExitGraceMs ?? 5e3;
3929
+ this.snapshotCacheMs = options.snapshotCacheMs ?? 0;
3767
3930
  }
3768
- list() {
3769
- this.sweepRegistry();
3931
+ async list() {
3932
+ const snapshot = await this.getProcessSnapshot();
3933
+ await this.sweepRegistry(snapshot);
3770
3934
  for (const record of [...this.managedByInstanceId.values(), ...this.pending]) {
3771
- this.refresh(record);
3935
+ await this.refresh(record, snapshot);
3772
3936
  }
3773
3937
  const records = [...this.managedByInstanceId.values(), ...this.pending];
3774
- for (const registryRecord of this.registry.listOpen(this.registrySweepOptions())) {
3938
+ for (const registryRecord of await this.registry.listOpenUnchecked()) {
3775
3939
  const record = this.fromRegistryRecord(registryRecord);
3776
3940
  if (records.some((existing) => record.recordId && existing.recordId === record.recordId || record.instanceId && existing.instanceId === record.instanceId)) {
3777
3941
  continue;
@@ -3780,27 +3944,30 @@ var init_studio_instance_manager = __esm({
3780
3944
  }
3781
3945
  return records.filter((record) => record.closedAt === void 0).filter((instance, index, all) => all.indexOf(instance) === index);
3782
3946
  }
3783
- get(instanceId) {
3784
- this.sweepRegistry();
3947
+ async get(instanceId) {
3948
+ const snapshot = await this.getProcessSnapshot();
3949
+ await this.sweepRegistry(snapshot);
3785
3950
  const memoryRecord = this.managedByInstanceId.get(instanceId);
3786
3951
  if (memoryRecord)
3787
- return this.refresh(memoryRecord);
3788
- const registryRecord = this.registry.findAnyByInstanceId(instanceId);
3789
- return registryRecord ? this.refresh(this.fromRegistryRecord(registryRecord)) : void 0;
3952
+ return this.refresh(memoryRecord, snapshot);
3953
+ const registryRecord = await this.registry.findAnyByInstanceId(instanceId);
3954
+ return registryRecord ? this.refresh(this.fromRegistryRecord(registryRecord), snapshot) : void 0;
3790
3955
  }
3791
- getByLaunchId(launchId) {
3792
- this.sweepRegistry();
3956
+ async getByLaunchId(launchId) {
3957
+ const snapshot = await this.getProcessSnapshot();
3958
+ await this.sweepRegistry(snapshot);
3793
3959
  const memoryRecord = [...this.managedByInstanceId.values(), ...this.pending].find((record) => record.recordId === launchId);
3794
3960
  if (memoryRecord)
3795
- return this.refresh(memoryRecord);
3796
- const registryRecord = this.registry.findAnyByRecordId(launchId, this.registrySweepOptions());
3797
- return registryRecord ? this.refresh(this.fromRegistryRecord(registryRecord)) : void 0;
3961
+ return this.refresh(memoryRecord, snapshot);
3962
+ const registryRecord = await this.registry.findAnyByRecordId(launchId);
3963
+ return registryRecord ? this.refresh(this.fromRegistryRecord(registryRecord), snapshot) : void 0;
3798
3964
  }
3799
- pendingLaunches() {
3965
+ async pendingLaunches() {
3800
3966
  const now = Date.now();
3967
+ const bootId = await this.getCurrentBootId();
3801
3968
  const records = [...this.pending];
3802
- for (const registryRecord of this.registry.listOpenUnchecked()) {
3803
- if (registryRecord.bootId !== this.getCurrentBootId())
3969
+ for (const registryRecord of await this.registry.listOpenUnchecked()) {
3970
+ if (registryRecord.bootId !== bootId)
3804
3971
  continue;
3805
3972
  if (records.some((record) => record.recordId === registryRecord.recordId))
3806
3973
  continue;
@@ -3808,8 +3975,9 @@ var init_studio_instance_manager = __esm({
3808
3975
  }
3809
3976
  return records.filter((record) => record.instanceId === void 0).filter((record) => record.state === "launching").filter((record) => record.connectionDeadlineAt === void 0 || record.connectionDeadlineAt > now);
3810
3977
  }
3811
- attachInstanceId(record, instanceId) {
3812
- this.refresh(record);
3978
+ async attachInstanceId(record, instanceId) {
3979
+ const snapshot = await this.getProcessSnapshot(true);
3980
+ await this.reconcileFromPositiveEvidence(record, snapshot);
3813
3981
  if (record.closedAt !== void 0 || record.state === "failed" || record.state === "exited")
3814
3982
  return;
3815
3983
  if (record.instanceId && record.instanceId !== instanceId)
@@ -3820,47 +3988,59 @@ var init_studio_instance_manager = __esm({
3820
3988
  this.clearConnectionTimer(record);
3821
3989
  this.pending.delete(record);
3822
3990
  this.managedByInstanceId.set(instanceId, record);
3823
- this.persist(record);
3991
+ await this.persist(record);
3824
3992
  }
3825
- markFailed(record, reason) {
3826
- this.refresh(record);
3993
+ async markFailed(record, reason) {
3827
3994
  if (record.closedAt !== void 0 || record.state !== "launching")
3828
3995
  return record;
3829
3996
  record.state = "failed";
3830
3997
  record.failedAt = Date.now();
3831
3998
  record.failureReason = reason;
3832
3999
  this.clearConnectionTimer(record);
3833
- this.persist(record);
4000
+ await this.persist(record);
3834
4001
  return record;
3835
4002
  }
3836
- refresh(record) {
4003
+ async refresh(record, providedSnapshot) {
4004
+ if (record.closedAt !== void 0)
4005
+ return record;
4006
+ const snapshot = providedSnapshot ?? await this.getProcessSnapshot();
4007
+ await this.applyProcessObservation(record, this.observeRecord(record, snapshot));
3837
4008
  if (record.closedAt !== void 0)
3838
4009
  return record;
3839
- const processId = record.nativeProcessId ?? record.spawnPid;
3840
- const studioProcess = processId ? this.findProcessById(processId) : void 0;
3841
- if (processId && (!studioProcess || !this.verifyProcessForRecord(record, studioProcess))) {
3842
- return this.markProcessExited(record, void 0, studioProcess ? "Studio process identity changed; the retained PID was not reused." : record.instanceId ? "Studio process exited." : "Studio process exited before the MCP plugin connected.");
3843
- }
3844
4010
  if (record.state === "launching" && record.connectionDeadlineAt !== void 0 && Date.now() >= record.connectionDeadlineAt) {
3845
4011
  record.state = "failed";
3846
4012
  record.failedAt = Date.now();
3847
4013
  record.failureReason = "Studio launched, but the MCP plugin did not connect before timeout.";
3848
4014
  this.clearConnectionTimer(record);
3849
- this.persist(record);
4015
+ await this.persist(record);
3850
4016
  }
3851
4017
  return record;
3852
4018
  }
3853
4019
  async launch(options) {
3854
- this.sweepRegistry();
4020
+ const previous = this.launchQueue;
4021
+ let release;
4022
+ this.launchQueue = new Promise((resolve5) => {
4023
+ release = resolve5;
4024
+ });
4025
+ await previous;
4026
+ try {
4027
+ return await this.launchSerialized(options);
4028
+ } finally {
4029
+ release();
4030
+ }
4031
+ }
4032
+ async launchSerialized(options) {
4033
+ const initialSnapshot = await this.getProcessSnapshot(true);
4034
+ await this.sweepRegistry(initialSnapshot);
3855
4035
  const processEnvironment = parseStudioProcessEnvironmentPatch(options.processEnvironment);
3856
4036
  if (options.studioExecutable !== void 0 && (typeof options.studioExecutable !== "string" || options.studioExecutable.length === 0 || options.studioExecutable.includes("\0"))) {
3857
4037
  throw new Error("studio_executable must be a non-empty string without null characters.");
3858
4038
  }
3859
4039
  const preparedOptions = prepareStudioLaunchOptions(options);
3860
- const bootId = this.getCurrentBootId();
3861
- const before = new Set(this.listStudioProcesses().map((proc2) => proc2.Id));
3862
- const exe = preparedOptions.studioExecutable ?? this.processAdapter.resolveStudioExe?.() ?? resolveStudioExe();
3863
- const args = buildStudioLaunchArgs(preparedOptions).map(toStudioLaunchArg);
4040
+ const bootId = await this.getCurrentBootId();
4041
+ const before = new Set(initialSnapshot.status === "ok" ? initialSnapshot.processes.map((proc2) => proc2.Id) : []);
4042
+ const exe = preparedOptions.studioExecutable ?? (this.processAdapter.resolveStudioExe ? await this.processAdapter.resolveStudioExe() : await resolveStudioExeAsync());
4043
+ const args = await Promise.all(buildStudioLaunchArgs(preparedOptions).map(toStudioLaunchArgAsync));
3864
4044
  const spawnOptions = {
3865
4045
  cwd: isWsl() && existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd(),
3866
4046
  detached: true,
@@ -3870,9 +4050,9 @@ var init_studio_instance_manager = __esm({
3870
4050
  let proc;
3871
4051
  try {
3872
4052
  if (this.processAdapter.spawnStudio) {
3873
- proc = this.processAdapter.spawnStudio(exe, args, spawnOptions);
4053
+ proc = await this.processAdapter.spawnStudio(exe, args, spawnOptions);
3874
4054
  } else if (isWsl()) {
3875
- proc = spawnWindowsStudioFromWsl(exe, args, processEnvironment);
4055
+ proc = await spawnWindowsStudioFromWsl(exe, args, processEnvironment);
3876
4056
  } else {
3877
4057
  const child = spawn(exe, args, spawnOptions);
3878
4058
  proc = {
@@ -3891,8 +4071,9 @@ var init_studio_instance_manager = __esm({
3891
4071
  cleanupManagedBaseplateFiles({ source: preparedOptions.source, localPlaceFile: preparedOptions.localPlaceFile });
3892
4072
  throw error;
3893
4073
  }
4074
+ const launchedAt = Date.now();
3894
4075
  const record = {
3895
- recordId: randomUUID(),
4076
+ recordId: randomUUID2(),
3896
4077
  source: options.source,
3897
4078
  nativeProcessId: proc.nativePid,
3898
4079
  nativeProcessStartedAt: proc.nativeStartedAt,
@@ -3903,23 +4084,27 @@ var init_studio_instance_manager = __esm({
3903
4084
  universeId: preparedOptions.universeId,
3904
4085
  placeVersion: preparedOptions.placeVersion,
3905
4086
  localPlaceFile: preparedOptions.localPlaceFile,
3906
- launchedAt: Date.now(),
3907
- connectionDeadlineAt: Date.now() + (options.connectionTimeoutMs ?? 12e4),
4087
+ launchedAt,
4088
+ connectionDeadlineAt: launchedAt + (options.connectionTimeoutMs ?? 12e4),
3908
4089
  state: "launching",
3909
4090
  ownerPid: process.pid,
3910
4091
  bootId,
3911
- deleteLocalPlaceFileOnClose: options.source === "baseplate"
4092
+ deleteLocalPlaceFileOnClose: options.source === "baseplate",
4093
+ processObservationStatus: "running",
4094
+ lastProcessObservationAt: launchedAt,
4095
+ lastSuccessfulProcessObservationAt: launchedAt,
4096
+ consecutiveConfirmedMisses: 0
3912
4097
  };
3913
4098
  this.pending.add(record);
3914
4099
  try {
3915
- this.persist(record);
4100
+ await this.persist(record);
3916
4101
  } catch (error) {
3917
4102
  this.pending.delete(record);
3918
4103
  const processId = record.nativeProcessId ?? record.spawnPid;
3919
4104
  let stopError;
3920
4105
  if (processId) {
3921
4106
  try {
3922
- this.closeProcess(processId);
4107
+ await this.closeProcess(processId);
3923
4108
  } catch (caught) {
3924
4109
  stopError = caught;
3925
4110
  }
@@ -3931,38 +4116,40 @@ var init_studio_instance_manager = __esm({
3931
4116
  }
3932
4117
  proc.unref();
3933
4118
  proc.onExit?.((code, signal) => {
3934
- this.markProcessExited(record, code ?? void 0, signal ? `Studio process exited from signal ${signal}.` : record.instanceId ? "Studio process exited." : "Studio process exited before the MCP plugin connected.");
4119
+ this.runInBackground("persisting a Studio process exit", this.markProcessExited(record, code ?? void 0, signal ? `Studio process exited from signal ${signal}.` : record.instanceId ? "Studio process exited." : "Studio process exited before the MCP plugin connected."));
3935
4120
  });
3936
4121
  proc.onError?.((error) => {
3937
- this.markFailed(record, `Studio process failed to start: ${error.message}`);
4122
+ this.runInBackground("persisting a Studio process launch failure", this.markFailed(record, `Studio process failed to start: ${error.message}`));
3938
4123
  });
3939
4124
  const deadline = Date.now() + 5e3;
3940
4125
  while (Date.now() < deadline && record.nativeProcessId === void 0) {
3941
- const created = this.listStudioProcesses().find((candidate) => !before.has(candidate.Id));
4126
+ const snapshot = await this.getProcessSnapshot(true);
4127
+ const created = snapshot.status === "ok" ? snapshot.processes.find((candidate) => !before.has(candidate.Id)) : void 0;
3942
4128
  if (created) {
3943
4129
  record.nativeProcessId = created.Id;
3944
4130
  record.nativeProcessStartedAt = created.StartTimeUtcFileTime;
3945
- this.persist(record);
4131
+ await this.persist(record);
3946
4132
  break;
3947
4133
  }
3948
- await delay(250);
4134
+ await delay2(250);
3949
4135
  }
3950
4136
  if (record.nativeProcessId === void 0 && process.platform !== "win32" && !isWsl()) {
3951
4137
  record.nativeProcessId = proc.pid;
3952
- this.persist(record);
4138
+ await this.persist(record);
3953
4139
  }
3954
4140
  if (record.nativeProcessId !== void 0 && record.nativeProcessStartedAt === void 0) {
3955
- const nativeProcess = this.findProcessById(record.nativeProcessId);
4141
+ const snapshot = await this.getProcessSnapshot(true);
4142
+ const nativeProcess = snapshot.status === "ok" ? snapshot.processes.find((candidate) => candidate.Id === record.nativeProcessId) : void 0;
3956
4143
  if (nativeProcess?.StartTimeUtcFileTime !== void 0) {
3957
4144
  record.nativeProcessStartedAt = nativeProcess.StartTimeUtcFileTime;
3958
- this.persist(record);
4145
+ await this.persist(record);
3959
4146
  }
3960
4147
  }
3961
- this.startMonitor(record);
4148
+ this.startCoordinator(record);
3962
4149
  return record;
3963
4150
  }
3964
- closeByLaunchId(launchId) {
3965
- const record = this.getByLaunchId(launchId);
4151
+ async closeByLaunchId(launchId) {
4152
+ const record = await this.getByLaunchId(launchId);
3966
4153
  if (!record)
3967
4154
  return { status: "not_found", launchId };
3968
4155
  if (record.closedAt !== void 0) {
@@ -3970,19 +4157,19 @@ var init_studio_instance_manager = __esm({
3970
4157
  }
3971
4158
  return this.close(record);
3972
4159
  }
3973
- closeByInstanceId(instanceId) {
3974
- this.sweepRegistry();
4160
+ async closeByInstanceId(instanceId) {
4161
+ const snapshot = await this.getProcessSnapshot(true);
4162
+ await this.sweepRegistry(snapshot);
3975
4163
  const memoryRecord = this.managedByInstanceId.get(instanceId);
3976
4164
  if (memoryRecord)
3977
4165
  return this.close(memoryRecord);
3978
- const registryRecord = this.registry.findAnyByInstanceId(instanceId);
4166
+ const registryRecord = await this.registry.findAnyByInstanceId(instanceId);
3979
4167
  if (!registryRecord) {
3980
- this.sweepRegistry();
3981
4168
  return { status: "not_found", instanceId };
3982
4169
  }
3983
4170
  if (registryRecord.closedAt !== void 0) {
3984
4171
  this.cleanupManagedRecord(registryRecord);
3985
- this.registry.logEvent({
4172
+ await this.registry.logEvent({
3986
4173
  event: "registry_close_already_stopped",
3987
4174
  recordId: registryRecord.recordId,
3988
4175
  instanceId: registryRecord.instanceId,
@@ -3994,9 +4181,7 @@ var init_studio_instance_manager = __esm({
3994
4181
  }
3995
4182
  return this.close(this.fromRegistryRecord(registryRecord));
3996
4183
  }
3997
- close(record) {
3998
- this.stopMonitor(record);
3999
- this.refresh(record);
4184
+ async close(record) {
4000
4185
  if (record.closedAt !== void 0) {
4001
4186
  return { status: "already_closed", launchId: record.recordId, instanceId: record.instanceId };
4002
4187
  }
@@ -4004,36 +4189,33 @@ var init_studio_instance_manager = __esm({
4004
4189
  if (!processId) {
4005
4190
  throw new Error(`Cannot close ${record.instanceId ?? "Studio launch"} because its process id was not detected.`);
4006
4191
  }
4007
- const studioProcess = this.findProcessById(processId);
4008
- if (!studioProcess) {
4192
+ const snapshot = await this.getProcessSnapshot(true);
4193
+ const observation = this.observeRecord(record, snapshot);
4194
+ if (observation.status === "unknown") {
4195
+ await this.applyProcessObservation(record, observation);
4196
+ throw new Error(`Cannot verify the managed Studio process because process observation failed: ${observation.error}`);
4197
+ }
4198
+ if (observation.status === "not_running") {
4009
4199
  this.cleanupManagedRecord(record);
4010
- this.markProcessExited(record, void 0, record.failureReason);
4011
- this.registry.logEvent({
4200
+ await this.markProcessExited(record, void 0, observation.reason === "identity_mismatch" ? "Studio process identity changed; the retained PID was not reused." : record.failureReason);
4201
+ await this.registry.logEvent({
4012
4202
  event: "registry_close_already_stopped",
4013
4203
  recordId: record.recordId,
4014
4204
  instanceId: record.instanceId,
4015
4205
  source: record.source,
4016
- reason: "pid_not_running",
4206
+ reason: observation.reason === "identity_mismatch" ? "identity_mismatch" : "pid_not_running",
4017
4207
  action: "marked_closed_and_cleaned_baseplate"
4018
4208
  });
4019
4209
  return { status: "already_closed", launchId: record.recordId, instanceId: record.instanceId };
4020
4210
  }
4021
- if (!this.verifyProcessForRecord(record, studioProcess)) {
4022
- this.registry.logEvent({
4023
- event: "registry_process_verification_failed",
4024
- recordId: record.recordId,
4025
- instanceId: record.instanceId,
4026
- source: record.source,
4027
- reason: "identity_mismatch"
4028
- });
4029
- throw new Error("Managed Studio process identity could not be verified.");
4030
- }
4031
4211
  try {
4032
- this.closeProcess(processId);
4212
+ await this.closeProcess(processId);
4033
4213
  } catch (error) {
4034
- if (this.findProcessById(processId))
4214
+ const retry = await this.getProcessSnapshot(true);
4215
+ const retryObservation = this.observeRecord(record, retry);
4216
+ if (retryObservation.status === "running" || retryObservation.status === "unknown")
4035
4217
  throw error;
4036
- this.registry.logEvent({
4218
+ await this.registry.logEvent({
4037
4219
  event: "registry_close_already_stopped",
4038
4220
  recordId: record.recordId,
4039
4221
  instanceId: record.instanceId,
@@ -4042,7 +4224,7 @@ var init_studio_instance_manager = __esm({
4042
4224
  action: "marked_closed_and_cleaned_baseplate"
4043
4225
  });
4044
4226
  this.cleanupManagedRecord(record);
4045
- this.markProcessExited(record, void 0, record.failureReason);
4227
+ await this.markProcessExited(record, void 0, record.failureReason);
4046
4228
  return { status: "already_closed", launchId: record.recordId, instanceId: record.instanceId };
4047
4229
  }
4048
4230
  const closedAt = Date.now();
@@ -4050,25 +4232,33 @@ var init_studio_instance_manager = __esm({
4050
4232
  record.exitedAt = record.exitedAt ?? closedAt;
4051
4233
  if (record.state !== "failed")
4052
4234
  record.state = "exited";
4235
+ record.processObservationStatus = "not_running";
4236
+ record.lastProcessObservationAt = closedAt;
4237
+ record.lastSuccessfulProcessObservationAt = closedAt;
4238
+ record.lastProcessObservationError = void 0;
4053
4239
  this.cleanupManagedRecord(record);
4054
4240
  this.markClosedInMemory(record);
4055
- this.persist(record);
4241
+ await this.persist(record);
4056
4242
  return { status: "closed", launchId: record.recordId, instanceId: record.instanceId };
4057
4243
  }
4058
- closeConnectedInstance(instance) {
4059
- const process2 = this.findProcessForConnectedInstance(instance);
4244
+ async closeConnectedInstance(instance) {
4245
+ const snapshot = await this.getProcessSnapshot(true);
4246
+ if (snapshot.status === "error") {
4247
+ throw new Error(`Could not enumerate Studio processes: ${snapshot.error}`);
4248
+ }
4249
+ const process2 = this.findProcessForConnectedInstance(instance, snapshot.processes);
4060
4250
  if (!process2) {
4061
4251
  throw new Error(`Could not find a Studio process for connected instance "${instance.instanceId}".`);
4062
4252
  }
4063
- this.closeProcess(process2.Id);
4253
+ await this.closeProcess(process2.Id);
4064
4254
  }
4065
- closeProcess(processId) {
4255
+ async closeProcess(processId) {
4066
4256
  if (this.processAdapter.stopProcess) {
4067
- this.processAdapter.stopProcess(processId);
4257
+ await this.processAdapter.stopProcess(processId);
4068
4258
  return;
4069
4259
  }
4070
4260
  if (process.platform === "win32" || isWsl()) {
4071
- powershell(`Stop-Process -Id ${Math.trunc(processId)} -Force -ErrorAction Stop`);
4261
+ await powershellAsync(`Stop-Process -Id ${Math.trunc(processId)} -Force -ErrorAction Stop`);
4072
4262
  } else {
4073
4263
  try {
4074
4264
  process.kill(processId, "SIGTERM");
@@ -4078,8 +4268,7 @@ var init_studio_instance_manager = __esm({
4078
4268
  }
4079
4269
  }
4080
4270
  }
4081
- findProcessForConnectedInstance(instance) {
4082
- const processes = this.listStudioProcesses();
4271
+ findProcessForConnectedInstance(instance, processes) {
4083
4272
  if (processes.length === 0)
4084
4273
  return void 0;
4085
4274
  if (processes.length === 1)
@@ -4098,31 +4287,67 @@ var init_studio_instance_manager = __esm({
4098
4287
  }
4099
4288
  return void 0;
4100
4289
  }
4101
- listStudioProcesses() {
4102
- return this.processAdapter.listStudioProcesses?.() ?? listStudioProcesses();
4290
+ async getProcessSnapshot(force = false) {
4291
+ const now = Date.now();
4292
+ if (!force && this.snapshotCacheMs > 0 && this.cachedSnapshot && now - this.cachedSnapshot.observedAt <= this.snapshotCacheMs) {
4293
+ return this.cachedSnapshot;
4294
+ }
4295
+ if (this.snapshotInFlight)
4296
+ return this.snapshotInFlight;
4297
+ this.snapshotInFlight = (async () => {
4298
+ try {
4299
+ let snapshot;
4300
+ if (this.processAdapter.observeStudioProcesses) {
4301
+ snapshot = await this.processAdapter.observeStudioProcesses();
4302
+ } else if (this.processAdapter.listStudioProcesses) {
4303
+ const observedAt = Date.now();
4304
+ const processes = await this.processAdapter.listStudioProcesses();
4305
+ snapshot = { status: "ok", observedAt, processes };
4306
+ } else {
4307
+ snapshot = await observeStudioProcesses();
4308
+ }
4309
+ this.cachedSnapshot = snapshot;
4310
+ return snapshot;
4311
+ } catch (error) {
4312
+ const snapshot = {
4313
+ status: "error",
4314
+ observedAt: Date.now(),
4315
+ error: error instanceof Error ? error.message : String(error)
4316
+ };
4317
+ this.cachedSnapshot = snapshot;
4318
+ return snapshot;
4319
+ } finally {
4320
+ this.snapshotInFlight = void 0;
4321
+ }
4322
+ })();
4323
+ return this.snapshotInFlight;
4103
4324
  }
4104
- getCurrentBootId() {
4105
- return this.processAdapter.currentBootId?.() ?? currentBootId();
4325
+ async getCurrentBootId() {
4326
+ return this.processAdapter.currentBootId ? await this.processAdapter.currentBootId() : currentBootIdAsync();
4106
4327
  }
4107
- registrySweepOptions() {
4328
+ async registrySweepOptions(snapshot) {
4108
4329
  return {
4109
- currentBootId: this.getCurrentBootId(),
4110
- isProcessRunning: (record) => this.isRegistryProcessRunning(record),
4111
- cleanupRecord: (record) => this.cleanupManagedRecord(record)
4330
+ currentBootId: await this.getCurrentBootId(),
4331
+ observeProcess: (record) => this.observeRecord(this.fromRegistryRecord(record), snapshot),
4332
+ cleanupRecord: (record) => this.cleanupManagedRecord(record),
4333
+ confirmedExitMisses: this.confirmedExitMisses,
4334
+ confirmedExitGraceMs: this.confirmedExitGraceMs
4112
4335
  };
4113
4336
  }
4114
- sweepRegistry() {
4115
- this.registry.sweep(this.registrySweepOptions());
4337
+ async sweepRegistry(snapshot) {
4338
+ await this.registry.sweep(await this.registrySweepOptions(snapshot));
4116
4339
  }
4117
- findProcessById(processId) {
4118
- return this.listStudioProcesses().find((proc) => proc.Id === processId);
4119
- }
4120
- isRegistryProcessRunning(record) {
4340
+ observeRecord(record, snapshot) {
4341
+ if (snapshot.status === "error") {
4342
+ return { status: "unknown", observedAt: snapshot.observedAt, error: snapshot.error };
4343
+ }
4121
4344
  const processId = record.nativeProcessId ?? record.spawnPid;
4122
4345
  if (!processId)
4123
- return true;
4124
- const studioProcess = this.findProcessById(processId);
4125
- return !!studioProcess && this.verifyProcessForRecord(this.fromRegistryRecord(record), studioProcess);
4346
+ return { status: "running", observedAt: snapshot.observedAt };
4347
+ const studioProcess = snapshot.processes.find((candidate) => candidate.Id === processId);
4348
+ if (!studioProcess)
4349
+ return { status: "not_running", observedAt: snapshot.observedAt, reason: "missing" };
4350
+ return this.verifyProcessForRecord(record, studioProcess) ? { status: "running", observedAt: snapshot.observedAt } : { status: "not_running", observedAt: snapshot.observedAt, reason: "identity_mismatch" };
4126
4351
  }
4127
4352
  verifyProcessForRecord(record, studioProcess) {
4128
4353
  const processName = `${studioProcess.Name ?? ""} ${studioProcess.Path ?? ""}`.toLowerCase();
@@ -4156,9 +4381,9 @@ var init_studio_instance_manager = __esm({
4156
4381
  if (record.instanceId)
4157
4382
  this.managedByInstanceId.delete(record.instanceId);
4158
4383
  this.pending.delete(record);
4159
- this.stopMonitor(record);
4384
+ this.clearConnectionTimer(record);
4160
4385
  }
4161
- markProcessExited(record, exitCode, reason) {
4386
+ async markProcessExited(record, exitCode, reason) {
4162
4387
  if (record.closedAt !== void 0)
4163
4388
  return record;
4164
4389
  const exitedAt = Date.now();
@@ -4166,43 +4391,44 @@ var init_studio_instance_manager = __esm({
4166
4391
  record.closedAt = exitedAt;
4167
4392
  if (record.state !== "failed")
4168
4393
  record.state = "exited";
4394
+ record.processObservationStatus = "not_running";
4395
+ record.lastProcessObservationAt = exitedAt;
4396
+ record.lastSuccessfulProcessObservationAt = exitedAt;
4397
+ record.lastProcessObservationError = void 0;
4169
4398
  if (exitCode !== void 0)
4170
4399
  record.exitCode = exitCode;
4171
4400
  if (reason)
4172
4401
  record.failureReason = reason;
4173
4402
  this.cleanupManagedRecord(record);
4174
4403
  this.markClosedInMemory(record);
4175
- this.persist(record);
4404
+ await this.persist(record);
4176
4405
  return record;
4177
4406
  }
4178
- startMonitor(record) {
4179
- if (!record.recordId || record.closedAt !== void 0 || this.monitors.has(record.recordId))
4407
+ startCoordinator(record) {
4408
+ if (!record.recordId || record.closedAt !== void 0)
4180
4409
  return;
4181
4410
  if (record.state === "launching" && record.connectionDeadlineAt !== void 0) {
4182
4411
  const timeout = setTimeout(() => {
4183
- this.markFailed(record, "Studio launched, but the MCP plugin did not connect before timeout.");
4412
+ this.runInBackground("persisting a Studio plugin connection timeout", this.markFailed(record, "Studio launched, but the MCP plugin did not connect before timeout."));
4184
4413
  }, Math.max(0, record.connectionDeadlineAt - Date.now()));
4185
4414
  if (typeof timeout === "object" && "unref" in timeout)
4186
4415
  timeout.unref();
4187
4416
  this.connectionTimers.set(record.recordId, timeout);
4188
4417
  }
4189
- const timer = setInterval(() => {
4190
- this.refresh(record);
4191
- if (record.closedAt !== void 0)
4192
- this.stopMonitor(record);
4193
- }, 5e3);
4194
- if (typeof timer === "object" && "unref" in timer)
4195
- timer.unref();
4196
- this.monitors.set(record.recordId, timer);
4197
- }
4198
- stopMonitor(record) {
4199
- if (!record.recordId)
4418
+ if (this.coordinatorTimer)
4200
4419
  return;
4201
- const timer = this.monitors.get(record.recordId);
4202
- if (timer)
4203
- clearInterval(timer);
4204
- this.monitors.delete(record.recordId);
4205
- this.clearConnectionTimer(record);
4420
+ this.coordinatorTimer = setInterval(() => {
4421
+ if (this.coordinatorRefresh)
4422
+ return;
4423
+ this.coordinatorRefresh = this.refreshOwnedRecords().catch((error) => {
4424
+ this.reportBackgroundFailure("refreshing managed Studio records", error);
4425
+ }).finally(() => {
4426
+ this.coordinatorRefresh = void 0;
4427
+ });
4428
+ }, 5e3);
4429
+ if (typeof this.coordinatorTimer === "object" && "unref" in this.coordinatorTimer) {
4430
+ this.coordinatorTimer.unref();
4431
+ }
4206
4432
  }
4207
4433
  clearConnectionTimer(record) {
4208
4434
  if (!record.recordId)
@@ -4212,8 +4438,80 @@ var init_studio_instance_manager = __esm({
4212
4438
  clearTimeout(timer);
4213
4439
  this.connectionTimers.delete(record.recordId);
4214
4440
  }
4215
- persist(record) {
4216
- this.registry.upsert(this.toRegistryRecord(record));
4441
+ async refreshOwnedRecords() {
4442
+ const snapshot = await this.getProcessSnapshot(true);
4443
+ await this.sweepRegistry(snapshot);
4444
+ for (const record of [...this.managedByInstanceId.values(), ...this.pending]) {
4445
+ await this.refresh(record, snapshot);
4446
+ }
4447
+ }
4448
+ runInBackground(context, operation) {
4449
+ void operation.catch((error) => this.reportBackgroundFailure(context, error));
4450
+ }
4451
+ reportBackgroundFailure(context, error) {
4452
+ console.warn(`[robloxstudio-mcp] failed while ${context}: ${error instanceof Error ? error.message : String(error)}`);
4453
+ }
4454
+ async applyProcessObservation(record, observation) {
4455
+ if (record.closedAt !== void 0)
4456
+ return;
4457
+ const previousObservationAt = record.lastProcessObservationAt;
4458
+ record.lastProcessObservationAt = observation.observedAt;
4459
+ if (observation.status === "unknown") {
4460
+ record.processObservationStatus = "unknown";
4461
+ record.lastProcessObservationError = observation.error;
4462
+ record.consecutiveConfirmedMisses = 0;
4463
+ record.firstConfirmedMissAt = void 0;
4464
+ await this.persist(record);
4465
+ return;
4466
+ }
4467
+ record.lastSuccessfulProcessObservationAt = observation.observedAt;
4468
+ record.lastProcessObservationError = void 0;
4469
+ if (observation.status === "running") {
4470
+ record.processObservationStatus = "running";
4471
+ record.consecutiveConfirmedMisses = 0;
4472
+ record.firstConfirmedMissAt = void 0;
4473
+ await this.persist(record);
4474
+ return;
4475
+ }
4476
+ record.processObservationStatus = "not_running";
4477
+ if (previousObservationAt !== observation.observedAt) {
4478
+ record.consecutiveConfirmedMisses = (record.consecutiveConfirmedMisses ?? 0) + 1;
4479
+ record.firstConfirmedMissAt ??= observation.observedAt;
4480
+ }
4481
+ const confirmedAbsent = observation.reason === "identity_mismatch" || (record.consecutiveConfirmedMisses ?? 0) >= this.confirmedExitMisses && observation.observedAt - (record.firstConfirmedMissAt ?? observation.observedAt) >= this.confirmedExitGraceMs;
4482
+ if (!confirmedAbsent) {
4483
+ await this.persist(record);
4484
+ return;
4485
+ }
4486
+ await this.markProcessExited(record, void 0, observation.reason === "identity_mismatch" ? "Studio process identity changed; the retained PID was not reused." : record.instanceId ? "Studio process exited." : "Studio process exited before the MCP plugin connected.");
4487
+ }
4488
+ async reconcileFromPositiveEvidence(record, snapshot) {
4489
+ if (record.closedAt === void 0)
4490
+ return;
4491
+ if (record.failureReason !== "Studio process exited." && record.failureReason !== "Studio process exited before the MCP plugin connected.")
4492
+ return;
4493
+ if (snapshot.status !== "ok")
4494
+ return;
4495
+ const processId = record.nativeProcessId ?? record.spawnPid;
4496
+ const studioProcess = processId ? snapshot.processes.find((candidate) => candidate.Id === processId) : void 0;
4497
+ if (!studioProcess || !this.verifyProcessForRecord(record, studioProcess))
4498
+ return;
4499
+ if (record.exitCode !== void 0)
4500
+ return;
4501
+ record.closedAt = void 0;
4502
+ record.exitedAt = void 0;
4503
+ record.failureReason = void 0;
4504
+ record.state = record.instanceId ? "connected" : "launching";
4505
+ record.processObservationStatus = "running";
4506
+ record.lastProcessObservationAt = snapshot.observedAt;
4507
+ record.lastSuccessfulProcessObservationAt = snapshot.observedAt;
4508
+ record.lastProcessObservationError = void 0;
4509
+ record.consecutiveConfirmedMisses = 0;
4510
+ record.firstConfirmedMissAt = void 0;
4511
+ await this.persist(record);
4512
+ }
4513
+ async persist(record) {
4514
+ await this.registry.upsert(this.toRegistryRecord(record));
4217
4515
  }
4218
4516
  toRegistryRecord(record) {
4219
4517
  if (!record.recordId)
@@ -4245,7 +4543,13 @@ var init_studio_instance_manager = __esm({
4245
4543
  failureReason: record.failureReason,
4246
4544
  closedAt: record.closedAt,
4247
4545
  ownerPid: record.ownerPid,
4248
- bootId: record.bootId
4546
+ bootId: record.bootId,
4547
+ processObservationStatus: record.processObservationStatus,
4548
+ lastProcessObservationAt: record.lastProcessObservationAt,
4549
+ lastSuccessfulProcessObservationAt: record.lastSuccessfulProcessObservationAt,
4550
+ lastProcessObservationError: record.lastProcessObservationError,
4551
+ consecutiveConfirmedMisses: record.consecutiveConfirmedMisses,
4552
+ firstConfirmedMissAt: record.firstConfirmedMissAt
4249
4553
  };
4250
4554
  }
4251
4555
  fromRegistryRecord(record) {
@@ -4274,7 +4578,13 @@ var init_studio_instance_manager = __esm({
4274
4578
  closedAt: record.closedAt,
4275
4579
  ownerPid: record.ownerPid,
4276
4580
  bootId: record.bootId,
4277
- deleteLocalPlaceFileOnClose: record.deleteLocalPlaceFileOnClose
4581
+ deleteLocalPlaceFileOnClose: record.deleteLocalPlaceFileOnClose,
4582
+ processObservationStatus: record.processObservationStatus,
4583
+ lastProcessObservationAt: record.lastProcessObservationAt,
4584
+ lastSuccessfulProcessObservationAt: record.lastSuccessfulProcessObservationAt,
4585
+ lastProcessObservationError: record.lastProcessObservationError,
4586
+ consecutiveConfirmedMisses: record.consecutiveConfirmedMisses,
4587
+ firstConfirmedMissAt: record.firstConfirmedMissAt
4278
4588
  };
4279
4589
  }
4280
4590
  };
@@ -5085,7 +5395,7 @@ var init_esm = __esm({
5085
5395
 
5086
5396
  // ../core/dist/studio-skills.js
5087
5397
  import { createHash as createHash2 } from "crypto";
5088
- import { existsSync as existsSync4, readFileSync as readFileSync5, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
5398
+ import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
5089
5399
  import * as path4 from "path";
5090
5400
  function decompressLz4Block(input, outputLength) {
5091
5401
  const output = Buffer.allocUnsafe(outputLength);
@@ -5297,7 +5607,7 @@ function discoverNamedFile(root, fileName, depth = 0) {
5297
5607
  const matches = [];
5298
5608
  let entries;
5299
5609
  try {
5300
- entries = readdirSync3(root, { withFileTypes: true });
5610
+ entries = readdirSync2(root, { withFileTypes: true });
5301
5611
  } catch {
5302
5612
  return matches;
5303
5613
  }
@@ -5311,7 +5621,7 @@ function discoverNamedFile(root, fileName, depth = 0) {
5311
5621
  }
5312
5622
  return matches;
5313
5623
  }
5314
- function resolveAssistantBundlePath(studioExe = resolveStudioExe()) {
5624
+ function resolveAssistantBundlePath(studioExe) {
5315
5625
  const override = process.env.ROBLOX_STUDIO_ASSISTANT_BUNDLE;
5316
5626
  if (override) {
5317
5627
  if (!existsSync4(override)) {
@@ -5319,7 +5629,7 @@ function resolveAssistantBundlePath(studioExe = resolveStudioExe()) {
5319
5629
  }
5320
5630
  return override;
5321
5631
  }
5322
- const exeDirectory = path4.dirname(studioExe);
5632
+ const exeDirectory = path4.dirname(studioExe ?? resolveStudioExe());
5323
5633
  const roots = [
5324
5634
  path4.join(exeDirectory, "BuiltInStandalonePlugins"),
5325
5635
  path4.resolve(exeDirectory, "..", "Resources", "BuiltInStandalonePlugins"),
@@ -5333,18 +5643,18 @@ function resolveAssistantBundlePath(studioExe = resolveStudioExe()) {
5333
5643
  for (const candidate of discoverNamedFile(root, "Assistant.rbxm"))
5334
5644
  candidates.add(candidate);
5335
5645
  }
5336
- const newest = [...candidates].map((candidate) => ({ candidate, modifiedAt: statSync3(candidate).mtimeMs })).sort((left, right) => right.modifiedAt - left.modifiedAt)[0]?.candidate;
5646
+ const newest = [...candidates].map((candidate) => ({ candidate, modifiedAt: statSync2(candidate).mtimeMs })).sort((left, right) => right.modifiedAt - left.modifiedAt)[0]?.candidate;
5337
5647
  if (!newest) {
5338
5648
  throw new Error(`Studio Assistant bundle not found for ${studioExe}. Set ROBLOX_STUDIO_ASSISTANT_BUNDLE to the installed Assistant.rbxm path.`);
5339
5649
  }
5340
5650
  return newest;
5341
5651
  }
5342
5652
  function loadBuiltInStudioSkills(bundlePath = resolveAssistantBundlePath()) {
5343
- const stats = statSync3(bundlePath);
5653
+ const stats = statSync2(bundlePath);
5344
5654
  if (cachedBundle?.path === bundlePath && cachedBundle.modifiedAt === stats.mtimeMs && cachedBundle.size === stats.size) {
5345
5655
  return cachedBundle.value;
5346
5656
  }
5347
- const buffer = readFileSync5(bundlePath);
5657
+ const buffer = readFileSync4(bundlePath);
5348
5658
  const skills = parseBuiltInStudioSkills(buffer);
5349
5659
  if (skills.length === 0) {
5350
5660
  throw new Error(`No built-in skill documents found in ${bundlePath}`);
@@ -7061,13 +7371,20 @@ var init_tools = __esm({
7061
7371
  openCloudClient;
7062
7372
  cookieClient;
7063
7373
  instanceManager;
7374
+ managedConnectionAssociations = Promise.resolve();
7064
7375
  constructor(bridge) {
7065
7376
  this.client = new StudioHttpClient(bridge);
7066
7377
  this.bridge = bridge;
7067
7378
  this.openCloudClient = new OpenCloudClient();
7068
7379
  this.cookieClient = new RobloxCookieClient();
7069
7380
  this.instanceManager = new StudioInstanceManager();
7070
- this.bridge.onInstanceRegistered((instance) => this._associateManagedEditConnection(instance));
7381
+ this.bridge.onInstanceRegistered((instance) => {
7382
+ const instanceManager = this.instanceManager;
7383
+ const association = this.managedConnectionAssociations.then(() => this._associateManagedEditConnection(instance, instanceManager));
7384
+ this.managedConnectionAssociations = association.catch((error) => {
7385
+ console.warn(`[robloxstudio-mcp] managed Studio connection association failed: ${error instanceof Error ? error.message : String(error)}`);
7386
+ });
7387
+ });
7071
7388
  }
7072
7389
  _textResult(body) {
7073
7390
  return { content: [{ type: "text", text: JSON.stringify(body) }] };
@@ -8648,9 +8965,9 @@ ${code}`
8648
8965
  _publicInstanceKey(instance) {
8649
8966
  return `${instance.instanceId}:${instance.role}:${instance.connectedAt}`;
8650
8967
  }
8651
- _isLatestPublishedPlaceOpen(placeId) {
8968
+ async _isLatestPublishedPlaceOpen(placeId) {
8652
8969
  const publishedInstanceId2 = `place:${placeId}`;
8653
- return this.bridge.getPublicInstances().some((instance) => instance.placeId === placeId || instance.instanceId === publishedInstanceId2) || this.instanceManager.list().some((record) => record.closedAt === void 0 && record.source === "published_place" && record.placeId === placeId);
8970
+ return this.bridge.getPublicInstances().some((instance) => instance.placeId === placeId || instance.instanceId === publishedInstanceId2) || (await this.instanceManager.list()).some((record) => record.closedAt === void 0 && record.source === "published_place" && record.placeId === placeId);
8654
8971
  }
8655
8972
  _matchesManagedLaunch(record, instance) {
8656
8973
  if (record.source === "published_place") {
@@ -8662,12 +8979,12 @@ ${code}`
8662
8979
  }
8663
8980
  return true;
8664
8981
  }
8665
- _associateManagedEditConnection(instance) {
8982
+ async _associateManagedEditConnection(instance, instanceManager) {
8666
8983
  if (instance.role !== "edit")
8667
8984
  return;
8668
- const candidate = this.instanceManager.pendingLaunches().filter((record) => instance.connectedAt >= record.launchedAt - 1e3).filter((record) => this._matchesManagedLaunch(record, instance)).sort((a, b) => a.launchedAt - b.launchedAt)[0];
8985
+ const candidate = (await instanceManager.pendingLaunches()).filter((record) => instance.connectedAt >= record.launchedAt - 1e3).filter((record) => this._matchesManagedLaunch(record, instance)).sort((a, b) => a.launchedAt - b.launchedAt)[0];
8669
8986
  if (candidate)
8670
- this.instanceManager.attachInstanceId(candidate, instance.instanceId);
8987
+ await instanceManager.attachInstanceId(candidate, instance.instanceId);
8671
8988
  }
8672
8989
  async _deriveUniverseId(placeId) {
8673
8990
  const response = await fetch(`https://apis.roblox.com/universes/v1/places/${placeId}/universe`);
@@ -8684,7 +9001,7 @@ ${code}`
8684
9001
  async _waitForManagedEditConnection(record, beforeKeys, timeoutMs) {
8685
9002
  const deadline = Date.now() + timeoutMs;
8686
9003
  while (Date.now() < deadline) {
8687
- this.instanceManager.refresh(record);
9004
+ await this.instanceManager.refresh(record);
8688
9005
  if (record.state === "failed" || record.state === "exited" || record.closedAt !== void 0) {
8689
9006
  return void 0;
8690
9007
  }
@@ -8696,7 +9013,6 @@ ${code}`
8696
9013
  return void 0;
8697
9014
  }
8698
9015
  _managedStatus(record) {
8699
- this.instanceManager.refresh(record);
8700
9016
  const connected = record.instanceId ? this.bridge.getPublicInstances().filter((instance) => instance.instanceId === record.instanceId) : [];
8701
9017
  return {
8702
9018
  launch_id: record.recordId,
@@ -8704,7 +9020,12 @@ ${code}`
8704
9020
  managed: true,
8705
9021
  state: record.state,
8706
9022
  pid: record.nativeProcessId ?? record.spawnPid,
8707
- process_running: record.closedAt === void 0 && record.exitedAt === void 0,
9023
+ process_running: record.closedAt !== void 0 || record.exitedAt !== void 0 ? false : record.processObservationStatus === "running" ? true : record.processObservationStatus === "not_running" ? false : null,
9024
+ process_observation_status: record.processObservationStatus ?? "unknown",
9025
+ last_process_observation_at: record.lastProcessObservationAt ? new Date(record.lastProcessObservationAt).toISOString() : void 0,
9026
+ last_successful_process_observation_at: record.lastSuccessfulProcessObservationAt ? new Date(record.lastSuccessfulProcessObservationAt).toISOString() : void 0,
9027
+ last_process_observation_error: record.lastProcessObservationError,
9028
+ consecutive_confirmed_misses: record.consecutiveConfirmedMisses ?? 0,
8708
9029
  source: record.source,
8709
9030
  local_place_file: record.localPlaceFile,
8710
9031
  place_id: record.placeId,
@@ -8756,15 +9077,18 @@ ${code}`
8756
9077
  body.next_page_token = response.nextPageToken;
8757
9078
  return this._textResult(body);
8758
9079
  }
9080
+ if (action === "status" || action === "close") {
9081
+ await this.managedConnectionAssociations;
9082
+ }
8759
9083
  if (action === "status") {
8760
9084
  if (launch_id) {
8761
- const record2 = this.instanceManager.getByLaunchId(launch_id);
9085
+ const record2 = await this.instanceManager.getByLaunchId(launch_id);
8762
9086
  if (!record2)
8763
9087
  return this._textResult({ error: "Launch is not managed.", launch_id });
8764
9088
  return this._textResult(this._managedStatus(record2));
8765
9089
  }
8766
9090
  if (instance_id) {
8767
- const record2 = this.instanceManager.get(instance_id);
9091
+ const record2 = await this.instanceManager.get(instance_id);
8768
9092
  const connected2 = this.bridge.getPublicInstances().filter((instance) => instance.instanceId === instance_id);
8769
9093
  if (!record2 && connected2.length === 0) {
8770
9094
  return this._textResult({ error: "Instance is not connected or managed.", instance_id });
@@ -8781,7 +9105,7 @@ ${code}`
8781
9105
  });
8782
9106
  }
8783
9107
  return this._textResult({
8784
- managed: this.instanceManager.list().filter((record2) => record2.closedAt === void 0).map((record2) => this._managedStatus(record2)),
9108
+ managed: (await this.instanceManager.list()).filter((record2) => record2.closedAt === void 0).map((record2) => this._managedStatus(record2)),
8785
9109
  connected: this.bridge.getPublicInstances().map((instance) => ({
8786
9110
  instance_id: instance.instanceId,
8787
9111
  role: instance.role,
@@ -8793,11 +9117,11 @@ ${code}`
8793
9117
  if (action === "close") {
8794
9118
  let record2;
8795
9119
  if (launch_id) {
8796
- record2 = this.instanceManager.getByLaunchId(launch_id);
9120
+ record2 = await this.instanceManager.getByLaunchId(launch_id);
8797
9121
  if (!record2)
8798
9122
  return this._textResult({ error: "Launch is not managed.", launch_id });
8799
9123
  const connectedInstanceId = record2.instanceId;
8800
- const closeResult2 = record2.closedAt === void 0 ? this.instanceManager.close(record2) : { status: "already_closed" };
9124
+ const closeResult2 = record2.closedAt === void 0 ? await this.instanceManager.close(record2) : { status: "already_closed" };
8801
9125
  if (connectedInstanceId) {
8802
9126
  await this.bridge.unregisterInstanceIdEverywhere(connectedInstanceId);
8803
9127
  await sleep(500);
@@ -8810,13 +9134,13 @@ ${code}`
8810
9134
  });
8811
9135
  }
8812
9136
  if (instance_id) {
8813
- const recordBeforeClose = this.instanceManager.get(instance_id);
8814
- const managedClose = this.instanceManager.closeByInstanceId(instance_id);
9137
+ const recordBeforeClose = await this.instanceManager.get(instance_id);
9138
+ const managedClose = await this.instanceManager.closeByInstanceId(instance_id);
8815
9139
  if (managedClose.status !== "not_found") {
8816
9140
  await this.bridge.unregisterInstanceIdEverywhere(instance_id);
8817
9141
  await sleep(500);
8818
9142
  await this.bridge.unregisterInstanceIdEverywhere(instance_id);
8819
- const closedRecord = recordBeforeClose ?? (managedClose.launchId ? this.instanceManager.getByLaunchId(managedClose.launchId) : void 0);
9143
+ const closedRecord = managedClose.launchId ? await this.instanceManager.getByLaunchId(managedClose.launchId) : recordBeforeClose;
8820
9144
  return this._textResult({
8821
9145
  ...closedRecord ? this._managedStatus(closedRecord) : { instance_id },
8822
9146
  close_status: managedClose.status,
@@ -8832,7 +9156,7 @@ ${code}`
8832
9156
  });
8833
9157
  }
8834
9158
  try {
8835
- this.instanceManager.closeConnectedInstance(edit);
9159
+ await this.instanceManager.closeConnectedInstance(edit);
8836
9160
  await sleep(500);
8837
9161
  } catch (error) {
8838
9162
  return this._textResult({
@@ -8847,7 +9171,7 @@ ${code}`
8847
9171
  message: "Studio instance closed."
8848
9172
  });
8849
9173
  } else {
8850
- const active = this.instanceManager.list().filter((entry) => entry.closedAt === void 0);
9174
+ const active = (await this.instanceManager.list()).filter((entry) => entry.closedAt === void 0);
8851
9175
  if (active.length === 0) {
8852
9176
  return this._textResult({ message: "No managed Studio instances are active." });
8853
9177
  }
@@ -8861,7 +9185,7 @@ ${code}`
8861
9185
  }
8862
9186
  if (record2.instanceId)
8863
9187
  await this.bridge.unregisterInstanceIdEverywhere(record2.instanceId);
8864
- const closeResult = this.instanceManager.close(record2);
9188
+ const closeResult = await this.instanceManager.close(record2);
8865
9189
  if (record2.instanceId) {
8866
9190
  await sleep(500);
8867
9191
  await this.bridge.unregisterInstanceIdEverywhere(record2.instanceId);
@@ -8888,7 +9212,7 @@ ${code}`
8888
9212
  studioExecutable = request.studio_executable;
8889
9213
  }
8890
9214
  const processEnvironment = parseStudioProcessEnvironmentPatch(request.process_environment);
8891
- if (launchSource === "published_place" && placeId !== void 0 && this._isLatestPublishedPlaceOpen(placeId)) {
9215
+ if (launchSource === "published_place" && placeId !== void 0 && await this._isLatestPublishedPlaceOpen(placeId)) {
8892
9216
  return this._textResult({
8893
9217
  error: "Place is already open.",
8894
9218
  message: `place_id ${placeId} is already connected. Use the existing instance or launch a specific place_revision.`
@@ -8917,11 +9241,11 @@ ${code}`
8917
9241
  const connected = await this._waitForManagedEditConnection(record, beforeKeys, timeoutMs);
8918
9242
  if (!connected) {
8919
9243
  if (record.state === "launching") {
8920
- this.instanceManager.markFailed(record, "Studio launched, but the MCP plugin did not connect before timeout.");
9244
+ await this.instanceManager.markFailed(record, "Studio launched, but the MCP plugin did not connect before timeout.");
8921
9245
  }
8922
9246
  if (record.closedAt === void 0) {
8923
9247
  try {
8924
- this.instanceManager.close(record);
9248
+ await this.instanceManager.close(record);
8925
9249
  } catch {
8926
9250
  }
8927
9251
  }
@@ -8930,7 +9254,7 @@ ${code}`
8930
9254
  error: record.failureReason ?? "Studio launched, but the MCP plugin did not connect before timeout."
8931
9255
  });
8932
9256
  }
8933
- this.instanceManager.attachInstanceId(record, connected.instanceId);
9257
+ await this.instanceManager.attachInstanceId(record, connected.instanceId);
8934
9258
  return this._textResult({
8935
9259
  ...this._managedStatus(record),
8936
9260
  message: launchSource === "place_revision" ? `Studio opened place revision ${placeVersion}.` : "Studio opened."
@@ -13788,7 +14112,7 @@ part(0,2,0,2,1,1,"b")`,
13788
14112
  });
13789
14113
 
13790
14114
  // ../core/dist/install-plugin-helpers.js
13791
- import { existsSync as existsSync6, readFileSync as readFileSync7, unlinkSync } from "fs";
14115
+ import { existsSync as existsSync6, readFileSync as readFileSync6, unlinkSync } from "fs";
13792
14116
  import { execSync } from "child_process";
13793
14117
  import { join as join6 } from "path";
13794
14118
  import { homedir as homedir5 } from "os";
@@ -13796,7 +14120,7 @@ function isWSL() {
13796
14120
  if (process.platform !== "linux")
13797
14121
  return false;
13798
14122
  try {
13799
- const v = readFileSync7("/proc/version", "utf8");
14123
+ const v = readFileSync6("/proc/version", "utf8");
13800
14124
  return /microsoft|wsl/i.test(v);
13801
14125
  } catch {
13802
14126
  return false;
@@ -13881,7 +14205,7 @@ __export(install_plugin_exports, {
13881
14205
  installBundledPlugin: () => installBundledPlugin,
13882
14206
  installPlugin: () => installPlugin
13883
14207
  });
13884
- import { copyFileSync as copyFileSync2, createWriteStream, existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync8, unlinkSync as unlinkSync2 } from "fs";
14208
+ import { copyFileSync as copyFileSync2, createWriteStream, existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync7, unlinkSync as unlinkSync2 } from "fs";
13885
14209
  import { dirname as dirname5, join as join7 } from "path";
13886
14210
  import { fileURLToPath } from "url";
13887
14211
  import { get } from "https";
@@ -13943,7 +14267,7 @@ function prepareInstall({
13943
14267
  }) {
13944
14268
  const pluginsFolder = getPluginsFolder();
13945
14269
  if (!existsSync7(pluginsFolder)) {
13946
- mkdirSync5(pluginsFolder, { recursive: true });
14270
+ mkdirSync4(pluginsFolder, { recursive: true });
13947
14271
  }
13948
14272
  handleVariantConflict({
13949
14273
  pluginsFolder,
@@ -13964,14 +14288,14 @@ function bundledAssetPath() {
13964
14288
  }
13965
14289
  function packageVersion() {
13966
14290
  const currentDir = dirname5(fileURLToPath(import.meta.url));
13967
- const pkg = JSON.parse(readFileSync8(join7(currentDir, "..", "package.json"), "utf8"));
14291
+ const pkg = JSON.parse(readFileSync7(join7(currentDir, "..", "package.json"), "utf8"));
13968
14292
  if (!pkg.version) {
13969
14293
  throw new Error("Package version not found");
13970
14294
  }
13971
14295
  return pkg.version;
13972
14296
  }
13973
14297
  function bundledPluginVersion(source) {
13974
- const match = readFileSync8(source, "utf8").match(/local CURRENT_VERSION = "([^"]+)"/);
14298
+ const match = readFileSync7(source, "utf8").match(/local CURRENT_VERSION = "([^"]+)"/);
13975
14299
  return match ? match[1] : null;
13976
14300
  }
13977
14301
  function assertBundledPluginVersion(source) {
@@ -13985,8 +14309,8 @@ function assertBundledPluginVersion(source) {
13985
14309
  }
13986
14310
  function filesMatch(a, b) {
13987
14311
  if (!existsSync7(b)) return false;
13988
- const aBytes = readFileSync8(a);
13989
- const bBytes = readFileSync8(b);
14312
+ const aBytes = readFileSync7(a);
14313
+ const bBytes = readFileSync7(b);
13990
14314
  return aBytes.length === bBytes.length && aBytes.equals(bBytes);
13991
14315
  }
13992
14316
  async function installBundledPlugin(options = {}) {