@quantiya/codevibe-antigravity-plugin 1.0.9 → 1.0.11

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 +70 -3
  2. package/package.json +2 -2
package/dist/server.js CHANGED
@@ -2789,6 +2789,7 @@ function truncate(s, maxBytes) {
2789
2789
 
2790
2790
  // src/server.ts
2791
2791
  var MOBILE_PROMPT_FLOOR_RECENCY_MS = 12e4;
2792
+ var LAUNCH_SETTLE_TIMEOUT_MS = 3e3;
2792
2793
  var McpServer = class {
2793
2794
  constructor(options) {
2794
2795
  /** Per-session causal floor for transcript-event timestamps emitted right
@@ -2836,6 +2837,19 @@ var McpServer = class {
2836
2837
  * process.exit before the original stop's cleanup completes.
2837
2838
  * (Stage 1 MED finding 2026-05-20.) */
2838
2839
  this.stopPromise = null;
2840
+ /** In-flight createLaunchSession() promise. doStop() awaits it so a
2841
+ * SIGTERM arriving DURING startup (while resumeOrCreateSession is still
2842
+ * creating the ACTIVE row) doesn't let the signal handler's
2843
+ * process.exit() fire before the just-created row is marked INACTIVE.
2844
+ * Resolved (instant) for the normal shutdown path. (#142 ii) */
2845
+ this.launchSessionPromise = null;
2846
+ /** The launch sessionId whose ACTIVE row has been (or is being) created by
2847
+ * an in-flight createLaunchSession(), until it is either wired into
2848
+ * this.session (happy path) or marked INACTIVE. doStop() reads it to
2849
+ * guarantee the INACTIVE write lands even if the Phase-0 create-wait timed
2850
+ * out mid-cleanup — separating the bounded "wait for create" from the
2851
+ * always-awaited "mark INACTIVE". (#142 Stage 2 R1 iter-2 HIGH.) */
2852
+ this.pendingLaunchSessionId = null;
2839
2853
  /** Listener references kept so doStop() can detach them. Without this,
2840
2854
  * a server restart (test mode or future hot-reload) accumulates
2841
2855
  * listeners on the shared module instances. (Stage 2 HIGH finding
@@ -2853,6 +2867,7 @@ var McpServer = class {
2853
2867
  this.tmuxTarget = options.tmuxTarget ?? process.env.CODEVIBE_AGY_TMUX_TARGET ?? null;
2854
2868
  this.wrapperPid = options.wrapperPid ?? null;
2855
2869
  this.cliLogPath = options.cliLogPath ?? null;
2870
+ this.launchSettleTimeoutMs = options.launchSettleTimeoutMs ?? LAUNCH_SETTLE_TIMEOUT_MS;
2856
2871
  this.appSyncClient = options.appSyncClient ?? new import_codevibe_core4.AppSyncClient();
2857
2872
  this.workingDirectory = process.cwd();
2858
2873
  this.approvalDetector = options.approvalDetector ?? new ApprovalDetector(options.detectorOptions);
@@ -2897,7 +2912,9 @@ var McpServer = class {
2897
2912
  }
2898
2913
  this.started = true;
2899
2914
  this.lifecycleGen++;
2915
+ this.registerSignalHandlers();
2900
2916
  await (0, import_codevibe_core4.registerDeviceEncryptionKey)(this.appSyncClient, logger);
2917
+ if (!this.started) return { httpPort: 0 };
2901
2918
  (0, import_codevibe_core4.startDeviceKeyWatcher)(this.appSyncClient, logger);
2902
2919
  try {
2903
2920
  const swept = await this.appSyncClient.sweepOrphanSessions({
@@ -2917,13 +2934,18 @@ var McpServer = class {
2917
2934
  error: error instanceof Error ? error.message : String(error)
2918
2935
  });
2919
2936
  }
2937
+ if (!this.started) return { httpPort: 0 };
2920
2938
  this.approvalDetector.start();
2921
2939
  this.addListener(this.approvalDetector, "pending-prompt", (state) => {
2922
2940
  void this.handlePendingPrompt(state).catch((err) => {
2923
2941
  logger.error("handlePendingPrompt failed", { error: String(err) });
2924
2942
  });
2925
2943
  });
2926
- await this.createLaunchSession();
2944
+ this.launchSessionPromise = this.createLaunchSession();
2945
+ await this.launchSessionPromise;
2946
+ if (!this.started) {
2947
+ return { httpPort: 0 };
2948
+ }
2927
2949
  this.addListener(this.transcriptTailer, "event", (emit) => {
2928
2950
  void this.handleTranscriptEmit(emit).catch((err) => {
2929
2951
  logger.error("handleTranscriptEmit failed", { error: String(err) });
@@ -2949,7 +2971,6 @@ var McpServer = class {
2949
2971
  logger.warn("No tmux target \u2014 pane observer disabled (mobile-only mode)");
2950
2972
  }
2951
2973
  const httpPort = await this.httpApi.start();
2952
- this.registerSignalHandlers();
2953
2974
  await fireDaemonBeacon("daemon_init_step", {
2954
2975
  step: "ready",
2955
2976
  outcome: "ok",
@@ -2972,6 +2993,24 @@ var McpServer = class {
2972
2993
  }
2973
2994
  async doStop() {
2974
2995
  this.started = false;
2996
+ if (this.launchSessionPromise) {
2997
+ const settled = this.launchSessionPromise.catch(() => void 0);
2998
+ let timer;
2999
+ const timeout = new Promise((resolve3) => {
3000
+ timer = setTimeout(resolve3, this.launchSettleTimeoutMs);
3001
+ timer.unref?.();
3002
+ });
3003
+ try {
3004
+ await Promise.race([settled, timeout]);
3005
+ } finally {
3006
+ if (timer) clearTimeout(timer);
3007
+ }
3008
+ }
3009
+ if (this.pendingLaunchSessionId) {
3010
+ const pendingId = this.pendingLaunchSessionId;
3011
+ this.pendingLaunchSessionId = null;
3012
+ await this.deactivateLaunchRow(pendingId);
3013
+ }
2975
3014
  if (this.subscription) {
2976
3015
  try {
2977
3016
  this.subscription();
@@ -3051,6 +3090,27 @@ var McpServer = class {
3051
3090
  launchSessionId: this.session.sessionId
3052
3091
  });
3053
3092
  }
3093
+ /**
3094
+ * Stop the heartbeat and mark a launch row INACTIVE. Best-effort + idempotent:
3095
+ * createLaunchSession's bail and doStop's pending-id guarantee may both call
3096
+ * this for the same id, but both write the SAME terminal status (INACTIVE) on
3097
+ * an unconditional last-writer-wins resolver, so a double/reordered write
3098
+ * can't clobber or resurrect ACTIVE. (Separate from the ACTIVE-vs-INACTIVE
3099
+ * per-session ordering that core 1.0.29's status-write chain handles.)
3100
+ * (#142 Stage 2 R1 iter-2.)
3101
+ */
3102
+ async deactivateLaunchRow(sessionId) {
3103
+ try {
3104
+ this.appSyncClient.stopHeartbeat(sessionId);
3105
+ await this.appSyncClient.updateSession({ sessionId, status: "INACTIVE" });
3106
+ logger.info("Marked superseded/interrupted launch session INACTIVE", { sessionId });
3107
+ } catch (err) {
3108
+ logger.warn("Failed to mark superseded/interrupted launch session INACTIVE", {
3109
+ sessionId,
3110
+ error: String(err)
3111
+ });
3112
+ }
3113
+ }
3054
3114
  /**
3055
3115
  * v9 launch-session: create the wrapper-lifetime backend session at
3056
3116
  * start(). All agy conv UUIDs observed during this lifetime emit
@@ -3063,6 +3123,7 @@ var McpServer = class {
3063
3123
  const sessionId = generateLaunchSessionId(this.wrapperPid ?? process.pid);
3064
3124
  const userId = this.appSyncClient.getCurrentUserId();
3065
3125
  const projectPath = process.cwd();
3126
+ this.pendingLaunchSessionId = sessionId;
3066
3127
  let sessionKey = null;
3067
3128
  try {
3068
3129
  const result = await (0, import_codevibe_core4.resumeOrCreateSession)(
@@ -3078,6 +3139,7 @@ var McpServer = class {
3078
3139
  );
3079
3140
  sessionKey = result.sessionKey ?? null;
3080
3141
  } catch (err) {
3142
+ this.pendingLaunchSessionId = null;
3081
3143
  const msg = String(err);
3082
3144
  if (msg.includes("session-limit-exceeded")) {
3083
3145
  logger.warn("Free-tier session limit reached \u2014 mobile sync disabled for this wrapper", { sessionId });
@@ -3088,7 +3150,11 @@ var McpServer = class {
3088
3150
  logger.error("createLaunchSession failed (non-fatal)", { sessionId, error: msg });
3089
3151
  return;
3090
3152
  }
3091
- if (gen !== this.lifecycleGen || !this.started) return;
3153
+ if (gen !== this.lifecycleGen || !this.started) {
3154
+ await this.deactivateLaunchRow(sessionId);
3155
+ if (this.pendingLaunchSessionId === sessionId) this.pendingLaunchSessionId = null;
3156
+ return;
3157
+ }
3092
3158
  this.session = {
3093
3159
  sessionId,
3094
3160
  conversationId: "",
@@ -3101,6 +3167,7 @@ var McpServer = class {
3101
3167
  metadata: { wrapperPid: this.wrapperPid ?? void 0, launch: true },
3102
3168
  sessionKey
3103
3169
  };
3170
+ this.pendingLaunchSessionId = null;
3104
3171
  try {
3105
3172
  const stop = this.appSyncClient.subscribeToEvents(
3106
3173
  sessionId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quantiya/codevibe-antigravity-plugin",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
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
  "bin": {
@@ -48,7 +48,7 @@
48
48
  "node": ">=18.0.0"
49
49
  },
50
50
  "dependencies": {
51
- "@quantiya/codevibe-core": "^1.0.28",
51
+ "@quantiya/codevibe-core": "^1.0.30",
52
52
  "chokidar": "^5.0.0",
53
53
  "dotenv": "^16.6.1",
54
54
  "express": "^5.1.0",