@quantiya/codevibe-antigravity-plugin 2.0.50 → 2.0.51

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/server.js +221 -38
  2. package/package.json +2 -2
package/dist/server.js CHANGED
@@ -290,7 +290,64 @@ var fs2 = __toESM(require("fs"));
290
290
  var os2 = __toESM(require("os"));
291
291
  var path2 = __toESM(require("path"));
292
292
  var import_chokidar = require("chokidar");
293
+
294
+ // src/shutdown-bounds.ts
295
+ var TEARDOWN_STEP_TIMEOUT_MS = 3e3;
296
+ var WATCHER_CLOSE_TIMEOUT_MS = 2e3;
297
+ var SHUTDOWN_WATCHDOG_MS = 15e3;
298
+ async function boundedStep(work, ms, onTimeout) {
299
+ let timer;
300
+ try {
301
+ const started = (() => {
302
+ try {
303
+ return Promise.resolve(work());
304
+ } catch (err) {
305
+ return Promise.reject(err);
306
+ }
307
+ })();
308
+ const timeout = new Promise((resolve3) => {
309
+ timer = setTimeout(() => resolve3("timeout"), ms);
310
+ if (timer.unref) timer.unref();
311
+ });
312
+ const outcome = await Promise.race([
313
+ started.then(
314
+ () => "settled",
315
+ () => "settled"
316
+ ),
317
+ timeout
318
+ ]);
319
+ if (outcome === "timeout") {
320
+ void started.catch(() => void 0);
321
+ try {
322
+ onTimeout?.();
323
+ } catch {
324
+ }
325
+ }
326
+ return outcome;
327
+ } finally {
328
+ if (timer) clearTimeout(timer);
329
+ }
330
+ }
331
+ function armShutdownWatchdog(exit, onFire, ms = SHUTDOWN_WATCHDOG_MS, durableObligationSettled = () => true) {
332
+ const timer = setTimeout(() => {
333
+ try {
334
+ onFire();
335
+ } catch {
336
+ }
337
+ let settled = false;
338
+ try {
339
+ settled = durableObligationSettled();
340
+ } catch {
341
+ settled = false;
342
+ }
343
+ exit(settled ? 0 : 1);
344
+ }, ms);
345
+ return () => clearTimeout(timer);
346
+ }
347
+
348
+ // src/transcript-tailer.ts
293
349
  var OFFSET_FLUSH_INTERVAL_MS = 3e4;
350
+ var ERROR_LOG_MIN_INTERVAL_MS = 1e4;
294
351
  var LRU_CAP = 50;
295
352
  var TRANSCRIPT_FILE_REGEX = /\/brain\/([0-9a-fA-F-]{36})\/\.system_generated\/logs\/transcript\.jsonl$/;
296
353
  function defaultPaths() {
@@ -306,6 +363,11 @@ var TranscriptTailer = class extends import_events.EventEmitter {
306
363
  this.watcher = null;
307
364
  this.isWatching = false;
308
365
  this.isShuttingDown = false;
366
+ /** Error-log rate limiting (incident 2026-09-22 — a wedged watcher wrote
367
+ * 356,298 identical EMFILE errors / 96 MB before the daemon was killed). */
368
+ this.lastErrorSignature = null;
369
+ this.lastErrorLoggedAtMs = 0;
370
+ this.suppressedErrorCount = 0;
309
371
  /** Per-file offset state (in-memory mirror of the persisted store). */
310
372
  this.offsets = /* @__PURE__ */ new Map();
311
373
  /** Conversation UUIDs we're actively tailing. v1.0 pins to single-active
@@ -383,8 +445,7 @@ var TranscriptTailer = class extends import_events.EventEmitter {
383
445
  this.watcher.on("change", (filePath) => this.onFileChanged(filePath));
384
446
  this.watcher.on("unlink", (filePath) => this.onFileUnlinked(filePath));
385
447
  this.watcher.on("error", (error) => {
386
- logger.error("Chokidar error", { error });
387
- this.emitError(error);
448
+ this.emitError(error, "Chokidar error");
388
449
  });
389
450
  this.watcher.on("ready", () => {
390
451
  logger.info("Transcript tailer ready (chokidar initial scan complete)");
@@ -404,15 +465,25 @@ var TranscriptTailer = class extends import_events.EventEmitter {
404
465
  * covers the residual windows — the doStop teardown detaches listeners
405
466
  * BEFORE the tailer stops, and any embedder that never attached one.
406
467
  */
407
- emitError(error) {
468
+ emitError(error, chokidarLabel) {
408
469
  const err = error instanceof Error ? error : new Error(String(error));
409
- if (this.listenerCount("error") > 0) {
410
- this.emit("error", err);
470
+ const signature = String(err);
471
+ const now = Date.now();
472
+ const suppressed = signature === this.lastErrorSignature && now - this.lastErrorLoggedAtMs < ERROR_LOG_MIN_INTERVAL_MS;
473
+ const hasListener = this.listenerCount("error") > 0;
474
+ if (suppressed) {
475
+ this.suppressedErrorCount++;
411
476
  } else {
412
- logger.error("Transcript tailer error (no listener attached \u2014 logged, non-fatal)", {
413
- error: String(err)
477
+ logger.error(chokidarLabel ?? "Transcript tailer error (non-fatal)", {
478
+ error: signature,
479
+ ...hasListener ? {} : { noListenerAttached: true },
480
+ ...this.suppressedErrorCount > 0 ? { suppressedRepeats: this.suppressedErrorCount } : {}
414
481
  });
482
+ this.lastErrorSignature = signature;
483
+ this.lastErrorLoggedAtMs = now;
484
+ this.suppressedErrorCount = 0;
415
485
  }
486
+ if (hasListener) this.emit("error", err);
416
487
  }
417
488
  /**
418
489
  * Stop watching. Flushes offsets one last time before closing chokidar.
@@ -432,10 +503,16 @@ var TranscriptTailer = class extends import_events.EventEmitter {
432
503
  logger.warn("Final offset flush failed", { error });
433
504
  }
434
505
  if (this.watcher) {
435
- try {
436
- await this.watcher.close();
437
- } catch (error) {
438
- logger.warn("Error closing chokidar watcher", { error });
506
+ const watcher = this.watcher;
507
+ const outcome = await boundedStep(
508
+ () => watcher.close(),
509
+ WATCHER_CLOSE_TIMEOUT_MS,
510
+ () => logger.warn("chokidar watcher.close() timed out \u2014 abandoning it", {
511
+ timeoutMs: WATCHER_CLOSE_TIMEOUT_MS
512
+ })
513
+ );
514
+ if (outcome === "timeout") {
515
+ logger.warn("Transcript tailer watcher abandoned; relying on shutdown watchdog");
439
516
  }
440
517
  this.watcher = null;
441
518
  }
@@ -443,7 +520,12 @@ var TranscriptTailer = class extends import_events.EventEmitter {
443
520
  this.pendingReads.clear();
444
521
  this.isWatching = false;
445
522
  this.isShuttingDown = false;
446
- logger.info("Transcript tailer stopped");
523
+ logger.info(
524
+ "Transcript tailer stopped",
525
+ this.suppressedErrorCount > 0 ? { suppressedErrorRepeats: this.suppressedErrorCount } : void 0
526
+ );
527
+ this.lastErrorSignature = null;
528
+ this.suppressedErrorCount = 0;
447
529
  }
448
530
  /**
449
531
  * Best-effort sync flush. Safe to call from a process EXIT trap.
@@ -3703,6 +3785,17 @@ var McpServer = class _McpServer {
3703
3785
  * stop() — preserving the monotonic invariant lets stale callbacks
3704
3786
  * reliably detect they're stale. (Codex Stage 2 R1 v5 MED 2026-05-20.) */
3705
3787
  this.lifecycleGen = 0;
3788
+ /** Tracks the durable settlement obligation for a launch row. Set when a
3789
+ * create is ATTEMPTED (before the await — the marker is pre-set), cleared on
3790
+ * a confirmed retirement AND when the create threw. Read by the shutdown
3791
+ * watchdog to choose its exit code. (R2-F1 / R3-F1 / R3-F2 — see
3792
+ * isDurableObligationSettled() for the full predicate and its accepted
3793
+ * residual.) */
3794
+ this.durableObligationOpen = false;
3795
+ /** Teardown steps that exceeded their bound during the current stop().
3796
+ * Surfaced in the shutdown summary so a wedge is visible in the log even
3797
+ * though it no longer blocks exit. (Incident 2026-09-22.) */
3798
+ this.teardownStepsTimedOut = [];
3706
3799
  /** In-flight stop() promise so a re-entrant call (e.g. double SIGINT)
3707
3800
  * awaits the same shutdown instead of bypassing it and calling
3708
3801
  * process.exit before the original stop's cleanup completes.
@@ -3852,8 +3945,7 @@ var McpServer = class _McpServer {
3852
3945
  if (!this.started) {
3853
3946
  return { httpPort: 0 };
3854
3947
  }
3855
- this.addListener(this.transcriptTailer, "error", (err) => {
3856
- logger.error("transcript tailer error (non-fatal)", { error: String(err) });
3948
+ this.addListener(this.transcriptTailer, "error", () => {
3857
3949
  });
3858
3950
  this.addListener(this.transcriptTailer, "event", (emit) => {
3859
3951
  const task = this.handleTranscriptEmit(emit).catch((err) => {
@@ -3915,6 +4007,7 @@ var McpServer = class _McpServer {
3915
4007
  }
3916
4008
  async doStop() {
3917
4009
  this.started = false;
4010
+ this.teardownStepsTimedOut = [];
3918
4011
  this.stopTmuxLifecycleMonitor();
3919
4012
  const launchSettlement = this.launchSessionPromise?.catch(() => void 0) ?? null;
3920
4013
  let mustAwaitLaunchSettlement = false;
@@ -3935,6 +4028,7 @@ var McpServer = class _McpServer {
3935
4028
  const pendingId = this.pendingLaunchSessionId;
3936
4029
  this.stopRequestedDuringCreate = true;
3937
4030
  const deactivated = await this.deactivateLaunchRow(pendingId);
4031
+ if (deactivated) this.durableObligationOpen = false;
3938
4032
  if (deactivated) {
3939
4033
  if (this.pendingLaunchSessionId === pendingId) this.pendingLaunchSessionId = null;
3940
4034
  } else {
@@ -3971,6 +4065,7 @@ var McpServer = class _McpServer {
3971
4065
  logger.warn("[tool-activity] Agy shutdown drain failed", { error: String(err) });
3972
4066
  }
3973
4067
  const retired = await this.deactivateLaunchRow(this.session.sessionId);
4068
+ if (retired) this.durableObligationOpen = false;
3974
4069
  if (!retired) {
3975
4070
  hostedRetirementError = new Error(
3976
4071
  `Failed to retire hosted Antigravity session ${this.session.sessionId}`
@@ -3984,26 +4079,10 @@ var McpServer = class _McpServer {
3984
4079
  }
3985
4080
  }
3986
4081
  this.unregisterSignalHandlers();
3987
- try {
3988
- await this.transcriptTailer.stop();
3989
- } catch (err) {
3990
- logger.warn("transcriptTailer.stop failed", { error: String(err) });
3991
- }
3992
- try {
3993
- await this.paneObserver.stop();
3994
- } catch (err) {
3995
- logger.warn("paneObserver.stop failed", { error: String(err) });
3996
- }
3997
- try {
3998
- await this.approvalDetector.stop();
3999
- } catch (err) {
4000
- logger.warn("approvalDetector.stop failed", { error: String(err) });
4001
- }
4002
- try {
4003
- await this.httpApi.stop();
4004
- } catch (err) {
4005
- logger.warn("httpApi.stop failed", { error: String(err) });
4006
- }
4082
+ await this.boundedTeardownStep("transcriptTailer.stop", () => this.transcriptTailer.stop());
4083
+ await this.boundedTeardownStep("paneObserver.stop", () => this.paneObserver.stop());
4084
+ await this.boundedTeardownStep("approvalDetector.stop", () => this.approvalDetector.stop());
4085
+ await this.boundedTeardownStep("httpApi.stop", () => this.httpApi.stop());
4007
4086
  try {
4008
4087
  this.appSyncClient.cleanupSubscriptions();
4009
4088
  } catch (err) {
@@ -4021,6 +4100,7 @@ var McpServer = class _McpServer {
4021
4100
  if (this.pendingLaunchSessionId) {
4022
4101
  const pendingId = this.pendingLaunchSessionId;
4023
4102
  const retired = await this.deactivateLaunchRow(pendingId);
4103
+ if (retired) this.durableObligationOpen = false;
4024
4104
  if (retired) {
4025
4105
  if (this.pendingLaunchSessionId === pendingId) this.pendingLaunchSessionId = null;
4026
4106
  } else {
@@ -4117,6 +4197,7 @@ var McpServer = class _McpServer {
4117
4197
  const userId = this.appSyncClient.getCurrentUserId();
4118
4198
  const projectPath = process.cwd();
4119
4199
  this.pendingLaunchSessionId = sessionId;
4200
+ this.durableObligationOpen = true;
4120
4201
  let sessionKey = null;
4121
4202
  let sessionKeyGen = null;
4122
4203
  try {
@@ -4138,7 +4219,10 @@ var McpServer = class _McpServer {
4138
4219
  sessionKey = result.sessionKey ?? null;
4139
4220
  sessionKeyGen = result.sessionKeyGen ?? null;
4140
4221
  } catch (err) {
4141
- if (this.pendingLaunchSessionId === sessionId) this.pendingLaunchSessionId = null;
4222
+ if (this.pendingLaunchSessionId === sessionId) {
4223
+ this.pendingLaunchSessionId = null;
4224
+ this.durableObligationOpen = false;
4225
+ }
4142
4226
  const msg = String(err);
4143
4227
  if (msg.includes("session-limit-exceeded")) {
4144
4228
  logger.warn("Free-tier session limit reached \u2014 mobile sync disabled for this wrapper", { sessionId });
@@ -4158,7 +4242,10 @@ var McpServer = class _McpServer {
4158
4242
  if (gen !== this.lifecycleGen || !this.started || this.stopRequestedDuringCreate) {
4159
4243
  const deactivated = await this.deactivateLaunchRow(sessionId);
4160
4244
  if (deactivated) {
4161
- if (this.pendingLaunchSessionId === sessionId) this.pendingLaunchSessionId = null;
4245
+ if (this.pendingLaunchSessionId === sessionId) {
4246
+ this.pendingLaunchSessionId = null;
4247
+ this.durableObligationOpen = false;
4248
+ }
4162
4249
  } else {
4163
4250
  logger.error(
4164
4251
  "createLaunchSession bail: INACTIVE write for the just-created row FAILED \u2014 settlement obligation remains open (marker kept); the row may leak ACTIVE if nothing retries before exit (#638 H4)",
@@ -5503,9 +5590,23 @@ var McpServer = class _McpServer {
5503
5590
  logger.info("Native Antigravity tmux session ended; stopping companion daemon", {
5504
5591
  tmuxTarget
5505
5592
  });
5593
+ const disarmLifecycleWatchdog = armShutdownWatchdog(
5594
+ (code) => process.exit(code),
5595
+ () => logger.error("Shutdown watchdog fired after native session end; exiting anyway", {
5596
+ watchdogMs: SHUTDOWN_WATCHDOG_MS,
5597
+ timedOutSteps: this.teardownStepsTimedOut,
5598
+ durableObligationSettled: this.isDurableObligationSettled()
5599
+ }),
5600
+ SHUTDOWN_WATCHDOG_MS,
5601
+ () => this.isDurableObligationSettled()
5602
+ );
5506
5603
  void this.stop().then(
5507
- () => process.exit(0),
5604
+ () => {
5605
+ disarmLifecycleWatchdog();
5606
+ process.exit(0);
5607
+ },
5508
5608
  (error) => {
5609
+ disarmLifecycleWatchdog();
5509
5610
  logger.error("Failed to stop companion daemon after native session end", {
5510
5611
  error: error instanceof Error ? error.message : String(error)
5511
5612
  });
@@ -5527,9 +5628,26 @@ var McpServer = class _McpServer {
5527
5628
  this.signalsRegistered = true;
5528
5629
  const onSignal = (sig) => {
5529
5630
  logger.info(`${sig} received \u2014 evaluating terminal shutdown`);
5631
+ const disarm = armShutdownWatchdog(
5632
+ (code) => process.exit(code),
5633
+ () => logger.error("Shutdown watchdog fired \u2014 teardown did not complete in time; exiting anyway", {
5634
+ signal: sig,
5635
+ watchdogMs: SHUTDOWN_WATCHDOG_MS,
5636
+ timedOutSteps: this.teardownStepsTimedOut,
5637
+ durableObligationSettled: this.isDurableObligationSettled()
5638
+ }),
5639
+ SHUTDOWN_WATCHDOG_MS,
5640
+ () => this.isDurableObligationSettled()
5641
+ );
5530
5642
  void this.stopForTerminalSignal(sig).then((stopped) => {
5531
- if (stopped) process.exit(0);
5643
+ if (stopped) {
5644
+ disarm();
5645
+ process.exit(0);
5646
+ } else {
5647
+ disarm();
5648
+ }
5532
5649
  }).catch((err) => {
5650
+ disarm();
5533
5651
  logger.error("shutdown error", { error: String(err) });
5534
5652
  process.exit(1);
5535
5653
  });
@@ -5566,6 +5684,71 @@ var McpServer = class _McpServer {
5566
5684
  await this.stop();
5567
5685
  return true;
5568
5686
  }
5687
+ /**
5688
+ * Has shutdown's DURABLE obligation landed? (Stage 1 r1 finding F3.)
5689
+ *
5690
+ * The only obligation that outliving this process can strand is the backend
5691
+ * session row being marked INACTIVE.
5692
+ *
5693
+ * R2-F1: an earlier version read `pendingLaunchSessionId === null &&
5694
+ * session === null`. That was lifecycle-WIRING state used as a proxy for the
5695
+ * durable OUTCOME, and it was wrong in BOTH directions:
5696
+ * (a) `this.session = null` is cleared UNCONDITIONALLY at the end of
5697
+ * Phase 3, including after a retirement that FAILED and set
5698
+ * `hostedRetirementError` — so it reported success on a row known to
5699
+ * be stranded ACTIVE, which is the exact laundering F3 asked to close;
5700
+ * (b) `session` is only cleared AFTER Phase 3, so while the watchdog was
5701
+ * outlasting a wedged Phase-3 step it still read non-null even though
5702
+ * the INACTIVE write had already succeeded — reporting failure for the
5703
+ * wedged-local-handle case that F3 explicitly wanted to keep at exit 0.
5704
+ *
5705
+ * `durableObligationOpen` tracks the outcome directly instead:
5706
+ * - SET when a create is ATTEMPTED (the marker is pre-set before the
5707
+ * await), so it means "a create was attempted", not "a row exists";
5708
+ * - CLEARED when `deactivateLaunchRow` returns true. The stale-lifecycle
5709
+ * clear is additionally identity-guarded because that branch's
5710
+ * precondition is that a NEWER lifecycle exists. The other confirmed
5711
+ * clears are NOT guarded, deliberately: none of them has "a newer
5712
+ * lifecycle exists" as a precondition, and reaching the hazard would
5713
+ * need start() to overlap an in-flight doStop() (doStop sets
5714
+ * started=false first). The Phase-2 clear additionally retires
5715
+ * this.session.sessionId and has no marker to compare against at all,
5716
+ * so "unguarded" there is a category difference, not an omission;
5717
+ * - CLEARED when the create THREW (R3-F2).
5718
+ *
5719
+ * ACCEPTED RESIDUAL: the last rule also clears for a lost-response create
5720
+ * (the server made the row, the response never arrived), so that case is
5721
+ * reported SETTLED even though a row may exist. This is the irreducible
5722
+ * SIGKILL-class residual the file already accepts for
5723
+ * `pendingLaunchSessionId`; the alternative left the flag permanently true
5724
+ * for every free-tier and unkeyed daemon. The row leaked before this change
5725
+ * too and a non-zero exit would not have retired it, so only a diagnostic
5726
+ * signal is traded away.
5727
+ */
5728
+ isDurableObligationSettled() {
5729
+ return !this.durableObligationOpen;
5730
+ }
5731
+ /**
5732
+ * Run one teardown step with a hard bound. Never throws and never hangs, so
5733
+ * a wedged step cannot prevent the steps after it from running.
5734
+ * (Incident 2026-09-22 — see src/shutdown-bounds.ts.)
5735
+ */
5736
+ async boundedTeardownStep(label, work) {
5737
+ const outcome = await boundedStep(
5738
+ async () => {
5739
+ try {
5740
+ await work();
5741
+ } catch (err) {
5742
+ logger.warn(`${label} failed`, { error: String(err) });
5743
+ }
5744
+ },
5745
+ TEARDOWN_STEP_TIMEOUT_MS,
5746
+ () => logger.warn(`${label} timed out \u2014 continuing teardown without it`, {
5747
+ timeoutMs: TEARDOWN_STEP_TIMEOUT_MS
5748
+ })
5749
+ );
5750
+ if (outcome === "timeout") this.teardownStepsTimedOut.push(label);
5751
+ }
5569
5752
  unregisterSignalHandlers() {
5570
5753
  if (!this.signalsRegistered) return;
5571
5754
  if (this.boundSigintHandler) process.off("SIGINT", this.boundSigintHandler);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quantiya/codevibe-antigravity-plugin",
3
- "version": "2.0.50",
3
+ "version": "2.0.51",
4
4
  "description": "Control Antigravity CLI from your iPhone and Android — real-time sync, approve file edits, send prompts by voice. Part of CodeVibe.",
5
5
  "main": "dist/server.js",
6
6
  "codevibe": {
@@ -46,7 +46,7 @@
46
46
  "node": ">=22.0.0"
47
47
  },
48
48
  "dependencies": {
49
- "@quantiya/codevibe-core": "^2.0.46",
49
+ "@quantiya/codevibe-core": "^2.0.48",
50
50
  "chokidar": "^5.0.0",
51
51
  "dotenv": "^16.6.1",
52
52
  "express": "^5.1.0",