@quantiya/codevibe-antigravity-plugin 1.0.8 → 1.0.10

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 +90 -17
  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,11 +2912,19 @@ 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({
2904
- agentType: "ANTIGRAVITY"
2921
+ agentType: "ANTIGRAVITY",
2922
+ // 6 min (core default is 15 min). A live daemon heartbeats every
2923
+ // 2 min, so a session stale >6 min has missed 3 beats and is
2924
+ // provably dead — safe to reap without false-positiving a
2925
+ // concurrently-running agy wrapper, while catching orphans on the
2926
+ // next start sooner. (agy orphaned-session fix 2026-05-31.)
2927
+ staleThresholdMs: 6 * 60 * 1e3
2905
2928
  });
2906
2929
  if (swept > 0) {
2907
2930
  logger.info("Orphan sweep: marked stale Antigravity sessions INACTIVE", { swept });
@@ -2911,13 +2934,18 @@ var McpServer = class {
2911
2934
  error: error instanceof Error ? error.message : String(error)
2912
2935
  });
2913
2936
  }
2937
+ if (!this.started) return { httpPort: 0 };
2914
2938
  this.approvalDetector.start();
2915
2939
  this.addListener(this.approvalDetector, "pending-prompt", (state) => {
2916
2940
  void this.handlePendingPrompt(state).catch((err) => {
2917
2941
  logger.error("handlePendingPrompt failed", { error: String(err) });
2918
2942
  });
2919
2943
  });
2920
- await this.createLaunchSession();
2944
+ this.launchSessionPromise = this.createLaunchSession();
2945
+ await this.launchSessionPromise;
2946
+ if (!this.started) {
2947
+ return { httpPort: 0 };
2948
+ }
2921
2949
  this.addListener(this.transcriptTailer, "event", (emit) => {
2922
2950
  void this.handleTranscriptEmit(emit).catch((err) => {
2923
2951
  logger.error("handleTranscriptEmit failed", { error: String(err) });
@@ -2943,7 +2971,6 @@ var McpServer = class {
2943
2971
  logger.warn("No tmux target \u2014 pane observer disabled (mobile-only mode)");
2944
2972
  }
2945
2973
  const httpPort = await this.httpApi.start();
2946
- this.registerSignalHandlers();
2947
2974
  await fireDaemonBeacon("daemon_init_step", {
2948
2975
  step: "ready",
2949
2976
  outcome: "ok",
@@ -2966,15 +2993,23 @@ var McpServer = class {
2966
2993
  }
2967
2994
  async doStop() {
2968
2995
  this.started = false;
2969
- try {
2970
- await this.transcriptTailer.stop();
2971
- } catch (err) {
2972
- logger.warn("transcriptTailer.stop failed", { error: String(err) });
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
+ }
2973
3008
  }
2974
- try {
2975
- await this.paneObserver.stop();
2976
- } catch (err) {
2977
- logger.warn("paneObserver.stop failed", { error: String(err) });
3009
+ if (this.pendingLaunchSessionId) {
3010
+ const pendingId = this.pendingLaunchSessionId;
3011
+ this.pendingLaunchSessionId = null;
3012
+ await this.deactivateLaunchRow(pendingId);
2978
3013
  }
2979
3014
  if (this.subscription) {
2980
3015
  try {
@@ -2990,8 +3025,11 @@ var McpServer = class {
2990
3025
  }
2991
3026
  }
2992
3027
  this.listenerHandles = [];
2993
- this.unregisterSignalHandlers();
2994
3028
  if (this.session) {
3029
+ try {
3030
+ this.appSyncClient.stopHeartbeat(this.session.sessionId);
3031
+ } catch {
3032
+ }
2995
3033
  try {
2996
3034
  await this.appSyncClient.updateSession({
2997
3035
  sessionId: this.session.sessionId,
@@ -3003,10 +3041,17 @@ var McpServer = class {
3003
3041
  error: String(err)
3004
3042
  });
3005
3043
  }
3006
- try {
3007
- this.appSyncClient.stopHeartbeat(this.session.sessionId);
3008
- } catch {
3009
- }
3044
+ }
3045
+ this.unregisterSignalHandlers();
3046
+ try {
3047
+ await this.transcriptTailer.stop();
3048
+ } catch (err) {
3049
+ logger.warn("transcriptTailer.stop failed", { error: String(err) });
3050
+ }
3051
+ try {
3052
+ await this.paneObserver.stop();
3053
+ } catch (err) {
3054
+ logger.warn("paneObserver.stop failed", { error: String(err) });
3010
3055
  }
3011
3056
  try {
3012
3057
  await this.approvalDetector.stop();
@@ -3045,6 +3090,27 @@ var McpServer = class {
3045
3090
  launchSessionId: this.session.sessionId
3046
3091
  });
3047
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
+ }
3048
3114
  /**
3049
3115
  * v9 launch-session: create the wrapper-lifetime backend session at
3050
3116
  * start(). All agy conv UUIDs observed during this lifetime emit
@@ -3057,6 +3123,7 @@ var McpServer = class {
3057
3123
  const sessionId = generateLaunchSessionId(this.wrapperPid ?? process.pid);
3058
3124
  const userId = this.appSyncClient.getCurrentUserId();
3059
3125
  const projectPath = process.cwd();
3126
+ this.pendingLaunchSessionId = sessionId;
3060
3127
  let sessionKey = null;
3061
3128
  try {
3062
3129
  const result = await (0, import_codevibe_core4.resumeOrCreateSession)(
@@ -3072,6 +3139,7 @@ var McpServer = class {
3072
3139
  );
3073
3140
  sessionKey = result.sessionKey ?? null;
3074
3141
  } catch (err) {
3142
+ this.pendingLaunchSessionId = null;
3075
3143
  const msg = String(err);
3076
3144
  if (msg.includes("session-limit-exceeded")) {
3077
3145
  logger.warn("Free-tier session limit reached \u2014 mobile sync disabled for this wrapper", { sessionId });
@@ -3082,7 +3150,11 @@ var McpServer = class {
3082
3150
  logger.error("createLaunchSession failed (non-fatal)", { sessionId, error: msg });
3083
3151
  return;
3084
3152
  }
3085
- 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
+ }
3086
3158
  this.session = {
3087
3159
  sessionId,
3088
3160
  conversationId: "",
@@ -3095,6 +3167,7 @@ var McpServer = class {
3095
3167
  metadata: { wrapperPid: this.wrapperPid ?? void 0, launch: true },
3096
3168
  sessionKey
3097
3169
  };
3170
+ this.pendingLaunchSessionId = null;
3098
3171
  try {
3099
3172
  const stop = this.appSyncClient.subscribeToEvents(
3100
3173
  sessionId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quantiya/codevibe-antigravity-plugin",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
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.29",
52
52
  "chokidar": "^5.0.0",
53
53
  "dotenv": "^16.6.1",
54
54
  "express": "^5.1.0",