@quantiya/codevibe-antigravity-plugin 2.0.17 → 2.0.19

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 +452 -62
  2. package/package.json +2 -2
package/dist/server.js CHANGED
@@ -46,7 +46,7 @@ var fs5 = __toESM(require("fs"));
46
46
  var os5 = __toESM(require("os"));
47
47
  var import_child_process3 = require("child_process");
48
48
  var import_util3 = require("util");
49
- var import_codevibe_core4 = require("@quantiya/codevibe-core");
49
+ var import_codevibe_core5 = require("@quantiya/codevibe-core");
50
50
 
51
51
  // src/logger.ts
52
52
  var import_os = __toESM(require("os"));
@@ -1961,7 +1961,7 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
1961
1961
  let matchedKey = null;
1962
1962
  for (const [key, call] of this.pendingCalls.entries()) {
1963
1963
  if (call.conversationId !== args.conversationId) continue;
1964
- if (call.toolType !== args.toolType) continue;
1964
+ if (args.toolType !== "GENERIC" && call.toolType !== args.toolType) continue;
1965
1965
  const expected = call.intentStepIndex + call.toolCallIndex + 1;
1966
1966
  if (expected !== args.resultStepIndex) continue;
1967
1967
  if (typeof args.resultByteOffset === "number") {
@@ -3010,6 +3010,255 @@ function truncate(s, maxBytes) {
3010
3010
  return s.slice(0, lo) + ` [...truncated ${s.length - lo} chars]`;
3011
3011
  }
3012
3012
 
3013
+ // src/tool-activity-integration.ts
3014
+ var import_codevibe_core4 = require("@quantiya/codevibe-core");
3015
+ var TOOL_ACTIVITY_CONTENT = "tool_activity";
3016
+ var DEFAULT_WINDOW_MS = 1e4;
3017
+ var DEFAULT_MAX_OCCURRENCES = 50;
3018
+ var DEFAULT_SHUTDOWN_DRAIN_MS = 3e3;
3019
+ function terminalSessionError(sessionId) {
3020
+ return Object.assign(new Error(`Antigravity session is no longer active: ${sessionId}`), {
3021
+ errorType: "SessionTerminal"
3022
+ });
3023
+ }
3024
+ function friendlyHookToolName(rawName, resultType) {
3025
+ switch (rawName?.toUpperCase()) {
3026
+ case "RUN_COMMAND":
3027
+ return "Bash";
3028
+ case "LIST_DIR":
3029
+ return "LS";
3030
+ case "GREP_SEARCH":
3031
+ return "Grep";
3032
+ case "VIEW_FILE":
3033
+ return "Read";
3034
+ case "WRITE_TO_FILE":
3035
+ case "REPLACE_FILE_CONTENT":
3036
+ case "MULTI_REPLACE_FILE_CONTENT":
3037
+ return "Edit";
3038
+ case "SEARCH_WEB":
3039
+ return "WebSearch";
3040
+ case "READ_URL_CONTENT":
3041
+ return "WebFetch";
3042
+ case "GENERATE_IMAGE":
3043
+ return "ImageGen";
3044
+ case "INVOKE_SUBAGENT":
3045
+ case "DEFINE_SUBAGENT":
3046
+ case "MANAGE_SUBAGENTS":
3047
+ return "Agent";
3048
+ case "SEND_MESSAGE":
3049
+ return "SendMessage";
3050
+ case "LIST_PERMISSIONS":
3051
+ return "Permissions";
3052
+ case "MANAGE_TASK":
3053
+ return "Task";
3054
+ case "SCHEDULE":
3055
+ return "Schedule";
3056
+ case "ASK_PERMISSION":
3057
+ case "ASK_QUESTION":
3058
+ return "AskUserQuestion";
3059
+ default:
3060
+ return transcriptTypeToFriendlyToolName(resultType);
3061
+ }
3062
+ }
3063
+ function commandTarget(command) {
3064
+ return command?.trim().split(/\s+/)[0] || void 0;
3065
+ }
3066
+ function agyOccurrenceId(emit, matchedCall) {
3067
+ const identity = matchedCall ? {
3068
+ conversationId: matchedCall.conversationId,
3069
+ intentCreatedAt: matchedCall.intentCreatedAt,
3070
+ intentStepIndex: matchedCall.intentStepIndex,
3071
+ toolCallIndex: matchedCall.toolCallIndex,
3072
+ rawToolName: matchedCall.rawToolName ?? null,
3073
+ command: matchedCall.command ?? null,
3074
+ filePath: matchedCall.filePath ?? null,
3075
+ cwd: matchedCall.cwd ?? null
3076
+ } : {
3077
+ conversationId: emit.conversationId,
3078
+ createdAt: emit.event.created_at,
3079
+ stepIndex: emit.event.step_index,
3080
+ type: emit.event.type,
3081
+ contentDigest: (0, import_codevibe_core4.digestOf)(emit.event.content ?? "")
3082
+ };
3083
+ return `agy:${(0, import_codevibe_core4.digestOf)(identity).slice("sha256:".length)}`;
3084
+ }
3085
+ var AgyToolActivityIntegration = class {
3086
+ constructor(deps) {
3087
+ this.consolidator = null;
3088
+ this.backendSessionId = null;
3089
+ this.sessionsToRehome = /* @__PURE__ */ new Set();
3090
+ this.deps = deps;
3091
+ }
3092
+ isActive() {
3093
+ return this.consolidator !== null;
3094
+ }
3095
+ async start(sessionId) {
3096
+ if (this.consolidator && this.backendSessionId === sessionId) return;
3097
+ if (this.consolidator) await this.stop();
3098
+ const outbox = new import_codevibe_core4.ToolActivityOutbox({
3099
+ agent: "agy",
3100
+ logger: this.deps.logger,
3101
+ root: this.deps.root
3102
+ });
3103
+ await this.rehomePendingWindows(outbox, sessionId);
3104
+ const ledger = new import_codevibe_core4.ToolActivityLedger({
3105
+ agent: "agy",
3106
+ sessionId,
3107
+ logger: this.deps.logger,
3108
+ root: this.deps.root
3109
+ });
3110
+ const consolidator = new import_codevibe_core4.ToolActivityConsolidator({
3111
+ agent: "agy",
3112
+ sessionId,
3113
+ logger: this.deps.logger,
3114
+ outbox,
3115
+ ledger,
3116
+ transport: this.buildTransport(),
3117
+ windowMs: this.deps.windowMs ?? DEFAULT_WINDOW_MS,
3118
+ maxOccurrences: DEFAULT_MAX_OCCURRENCES
3119
+ });
3120
+ this.backendSessionId = sessionId;
3121
+ try {
3122
+ await consolidator.init();
3123
+ this.consolidator = consolidator;
3124
+ this.deps.logger.info("[tool-activity] Agy consolidator started", { sessionId });
3125
+ } catch (error) {
3126
+ this.backendSessionId = null;
3127
+ await consolidator.shutdown().catch(() => void 0);
3128
+ throw error;
3129
+ }
3130
+ }
3131
+ async observeResult(emit, matchedCall) {
3132
+ const consolidator = this.consolidator;
3133
+ if (!consolidator) return void 0;
3134
+ const resultType = emit.event.type;
3135
+ const target = matchedCall?.filePath ?? commandTarget(matchedCall?.command) ?? matchedCall?.cwd;
3136
+ const inputIdentity = matchedCall ? {
3137
+ rawToolName: matchedCall.rawToolName ?? null,
3138
+ command: matchedCall.command ?? null,
3139
+ filePath: matchedCall.filePath ?? null,
3140
+ cwd: matchedCall.cwd ?? null
3141
+ } : void 0;
3142
+ return consolidator.observe({
3143
+ id: agyOccurrenceId(emit, matchedCall),
3144
+ tool: friendlyHookToolName(matchedCall?.rawToolName, resultType),
3145
+ ...target ? { normalizedTarget: target } : {},
3146
+ ts: emit.event.created_at,
3147
+ byteOffset: emit.byteOffset,
3148
+ ...inputIdentity ? { digest: (0, import_codevibe_core4.digestOf)(inputIdentity) } : {}
3149
+ });
3150
+ }
3151
+ async flush() {
3152
+ await this.consolidator?.flush();
3153
+ }
3154
+ async stop() {
3155
+ const consolidator = this.consolidator;
3156
+ this.consolidator = null;
3157
+ if (!consolidator) {
3158
+ this.backendSessionId = null;
3159
+ return;
3160
+ }
3161
+ await consolidator.drainSends(this.deps.shutdownDrainMs ?? DEFAULT_SHUTDOWN_DRAIN_MS).catch(() => void 0);
3162
+ await consolidator.shutdown();
3163
+ this.backendSessionId = null;
3164
+ }
3165
+ /** Preserve a backend-retired session's durable windows for the replacement. */
3166
+ async prepareForReplacement() {
3167
+ const consolidator = this.consolidator;
3168
+ const sessionId = this.backendSessionId;
3169
+ this.consolidator = null;
3170
+ this.backendSessionId = null;
3171
+ if (!consolidator || !sessionId) return;
3172
+ this.sessionsToRehome.add(sessionId);
3173
+ await consolidator.shutdown();
3174
+ await consolidator.drainSends(this.deps.shutdownDrainMs ?? DEFAULT_SHUTDOWN_DRAIN_MS).catch(() => void 0);
3175
+ }
3176
+ async rehomePendingWindows(outbox, replacementSessionId) {
3177
+ const sessionIds = await outbox.enumerateSessions();
3178
+ for (const oldSessionId of sessionIds) {
3179
+ if (oldSessionId === replacementSessionId) continue;
3180
+ const pending = await outbox.listPending(oldSessionId);
3181
+ if (pending.valid.length === 0) continue;
3182
+ let eligible = this.sessionsToRehome.has(oldSessionId);
3183
+ if (!eligible && this.deps.getBackendSession) {
3184
+ try {
3185
+ const oldSession = await this.deps.getBackendSession(oldSessionId);
3186
+ const replacement = this.deps.getSession();
3187
+ eligible = Boolean(
3188
+ oldSession && replacement && oldSession.status === "INACTIVE" && oldSession.userId === replacement.userId && oldSession.projectPath === replacement.projectPath
3189
+ );
3190
+ } catch (error) {
3191
+ this.deps.logger.warn("[tool-activity] Could not verify prior Agy session for rehome", {
3192
+ oldSessionId,
3193
+ error: String(error)
3194
+ });
3195
+ }
3196
+ }
3197
+ if (!eligible) continue;
3198
+ for (const envelope of pending.valid) {
3199
+ const rehomed = { ...envelope, backendSessionId: replacementSessionId };
3200
+ await outbox.persist(replacementSessionId, rehomed);
3201
+ await outbox.prune(oldSessionId, envelope.clientEventId);
3202
+ }
3203
+ this.sessionsToRehome.delete(oldSessionId);
3204
+ this.deps.logger.info("[tool-activity] Rehomed durable Agy windows", {
3205
+ oldSessionId,
3206
+ replacementSessionId,
3207
+ count: pending.valid.length
3208
+ });
3209
+ }
3210
+ }
3211
+ buildTransport() {
3212
+ const sendOnce = async (sessionId, envelope, override) => {
3213
+ const session = this.deps.getSession();
3214
+ if (!session || session.sessionId !== sessionId) {
3215
+ throw terminalSessionError(sessionId);
3216
+ }
3217
+ const encrypted = (0, import_codevibe_core4.encryptForEmit)(
3218
+ override?.sessionKey ?? session.sessionKey ?? null,
3219
+ session.sessionIsEncrypted ?? false,
3220
+ TOOL_ACTIVITY_CONTENT,
3221
+ { manifest: envelope.manifest }
3222
+ );
3223
+ if (!encrypted) {
3224
+ throw new Error("[tool-activity] encrypted Agy session has no key (retryable)");
3225
+ }
3226
+ const sessionKeyGen = override?.sessionKeyGen ?? session.sessionKeyGen;
3227
+ const input = {
3228
+ sessionId,
3229
+ type: import_codevibe_core4.EventType.TOOL_ACTIVITY,
3230
+ source: import_codevibe_core4.EventSource.DESKTOP,
3231
+ content: encrypted.content,
3232
+ metadata: encrypted.metadata,
3233
+ timestamp: envelope.windowStart,
3234
+ clientEventId: envelope.clientEventId,
3235
+ ...encrypted.isEncrypted ? { isEncrypted: true } : {},
3236
+ ...encrypted.isEncrypted && sessionKeyGen ? { expectedSessionKeyGen: sessionKeyGen } : {}
3237
+ };
3238
+ await this.deps.createEvent(input);
3239
+ };
3240
+ return {
3241
+ send: async (sessionId, envelope) => {
3242
+ try {
3243
+ await sendOnce(sessionId, envelope);
3244
+ } catch (error) {
3245
+ if ((0, import_codevibe_core4.isTerminalSessionError)(error)) {
3246
+ throw new Error(`[tool-activity] Agy session ${sessionId} retired; awaiting replacement`);
3247
+ }
3248
+ if ((0, import_codevibe_core4.isSessionKeyStaleError)(error) && this.deps.refreshSessionKey) {
3249
+ const refreshed = await this.deps.refreshSessionKey(sessionId);
3250
+ if (refreshed) {
3251
+ await sendOnce(sessionId, envelope, refreshed);
3252
+ return;
3253
+ }
3254
+ }
3255
+ throw error;
3256
+ }
3257
+ }
3258
+ };
3259
+ }
3260
+ };
3261
+
3013
3262
  // src/server.ts
3014
3263
  var MOBILE_PROMPT_FLOOR_RECENCY_MS = 12e4;
3015
3264
  var LAUNCH_SETTLE_TIMEOUT_MS = 3e3;
@@ -3051,6 +3300,8 @@ var McpServer = class _McpServer {
3051
3300
  * under this single sessionId. /resume does NOT create a new
3052
3301
  * session — same row. */
3053
3302
  this.session = null;
3303
+ this.transcriptHandlers = /* @__PURE__ */ new Set();
3304
+ this.toolActivityKeyRefresh = null;
3054
3305
  // E1 (§4/§6a-4) — per-promptId durable raise entries for the agy producer.
3055
3306
  // Keyed by the E1 promptId (= derive(ownerToken)); each holds the SECRET
3056
3307
  // ownerToken so a resume/retirement re-raise re-commits the SAME promptId
@@ -3191,7 +3442,21 @@ var McpServer = class _McpServer {
3191
3442
  this.wrapperPid = options.wrapperPid ?? null;
3192
3443
  this.cliLogPath = options.cliLogPath ?? null;
3193
3444
  this.launchSettleTimeoutMs = options.launchSettleTimeoutMs ?? LAUNCH_SETTLE_TIMEOUT_MS;
3194
- this.appSyncClient = options.appSyncClient ?? new import_codevibe_core4.AppSyncClient();
3445
+ this.appSyncClient = options.appSyncClient ?? new import_codevibe_core5.AppSyncClient();
3446
+ this.toolActivity = options.toolActivityIntegration === void 0 ? new AgyToolActivityIntegration({
3447
+ logger,
3448
+ createEvent: (input) => this.appSyncClient.createEvent(input),
3449
+ getSession: () => this.session,
3450
+ refreshSessionKey: (sessionId) => this.refreshToolActivitySessionKey(sessionId),
3451
+ getBackendSession: async (sessionId) => {
3452
+ const session = await this.appSyncClient.getSession(sessionId);
3453
+ return session ? {
3454
+ status: session.status,
3455
+ userId: session.userId,
3456
+ projectPath: session.projectPath
3457
+ } : null;
3458
+ }
3459
+ }) : options.toolActivityIntegration;
3195
3460
  this.workingDirectory = process.cwd();
3196
3461
  this.approvalDetector = options.approvalDetector ?? new ApprovalDetector(options.detectorOptions);
3197
3462
  this.paneObserver = options.paneObserver ?? new TmuxPaneObserver();
@@ -3243,9 +3508,9 @@ var McpServer = class _McpServer {
3243
3508
  this.stopRequestedDuringCreate = false;
3244
3509
  this.registerSignalHandlers();
3245
3510
  this.startTmuxLifecycleMonitor();
3246
- await (0, import_codevibe_core4.registerDeviceEncryptionKey)(this.appSyncClient, logger);
3511
+ await (0, import_codevibe_core5.registerDeviceEncryptionKey)(this.appSyncClient, logger);
3247
3512
  if (!this.started) return { httpPort: 0 };
3248
- (0, import_codevibe_core4.startDeviceKeyWatcher)(this.appSyncClient, logger);
3513
+ (0, import_codevibe_core5.startDeviceKeyWatcher)(this.appSyncClient, logger);
3249
3514
  try {
3250
3515
  const swept = await this.appSyncClient.sweepOrphanSessions({
3251
3516
  agentType: "ANTIGRAVITY",
@@ -3280,9 +3545,14 @@ var McpServer = class _McpServer {
3280
3545
  logger.error("transcript tailer error (non-fatal)", { error: String(err) });
3281
3546
  });
3282
3547
  this.addListener(this.transcriptTailer, "event", (emit) => {
3283
- void this.handleTranscriptEmit(emit).catch((err) => {
3548
+ const task = this.handleTranscriptEmit(emit).catch((err) => {
3284
3549
  logger.error("handleTranscriptEmit failed", { error: String(err) });
3285
3550
  });
3551
+ this.transcriptHandlers.add(task);
3552
+ void task.then(
3553
+ () => this.transcriptHandlers.delete(task),
3554
+ () => this.transcriptHandlers.delete(task)
3555
+ );
3286
3556
  });
3287
3557
  this.addListener(this.transcriptTailer, "conversation-discovered", (conversationId) => {
3288
3558
  void this.handleConversationDiscovered(conversationId).catch((err) => {
@@ -3378,17 +3648,29 @@ var McpServer = class _McpServer {
3378
3648
  }
3379
3649
  }
3380
3650
  this.listenerHandles = [];
3651
+ await Promise.allSettled([...this.transcriptHandlers]);
3381
3652
  if (this.session) {
3382
3653
  try {
3383
3654
  this.appSyncClient.stopHeartbeat(this.session.sessionId);
3384
3655
  } catch {
3385
3656
  }
3657
+ try {
3658
+ await this.toolActivity?.stop();
3659
+ } catch (err) {
3660
+ logger.warn("[tool-activity] Agy shutdown drain failed", { error: String(err) });
3661
+ }
3386
3662
  const retired = await this.deactivateLaunchRow(this.session.sessionId);
3387
3663
  if (!retired) {
3388
3664
  hostedRetirementError = new Error(
3389
3665
  `Failed to retire hosted Antigravity session ${this.session.sessionId}`
3390
3666
  );
3391
3667
  }
3668
+ } else {
3669
+ try {
3670
+ await this.toolActivity?.stop();
3671
+ } catch (err) {
3672
+ logger.warn("[tool-activity] Agy shutdown cleanup failed", { error: String(err) });
3673
+ }
3392
3674
  }
3393
3675
  this.unregisterSignalHandlers();
3394
3676
  try {
@@ -3525,8 +3807,9 @@ var McpServer = class _McpServer {
3525
3807
  const projectPath = process.cwd();
3526
3808
  this.pendingLaunchSessionId = sessionId;
3527
3809
  let sessionKey = null;
3810
+ let sessionKeyGen = null;
3528
3811
  try {
3529
- const result = await (0, import_codevibe_core4.resumeOrCreateSession)(
3812
+ const result = await (0, import_codevibe_core5.resumeOrCreateSession)(
3530
3813
  {
3531
3814
  sessionId,
3532
3815
  userId,
@@ -3542,6 +3825,7 @@ var McpServer = class _McpServer {
3542
3825
  logger
3543
3826
  );
3544
3827
  sessionKey = result.sessionKey ?? null;
3828
+ sessionKeyGen = result.sessionKeyGen ?? null;
3545
3829
  } catch (err) {
3546
3830
  if (this.pendingLaunchSessionId === sessionId) this.pendingLaunchSessionId = null;
3547
3831
  const msg = String(err);
@@ -3583,6 +3867,7 @@ var McpServer = class _McpServer {
3583
3867
  subscriptionActive: false,
3584
3868
  metadata: { wrapperPid: this.wrapperPid ?? void 0, launch: true },
3585
3869
  sessionKey,
3870
+ sessionKeyGen,
3586
3871
  // (#638) A null key here means a genuinely unencrypted/legacy session
3587
3872
  // (resumeOrCreateSession THROWS ENCRYPTED_SESSION_NO_KEY for an encrypted
3588
3873
  // session this device can't key, handled in the catch above — no session
@@ -3592,6 +3877,14 @@ var McpServer = class _McpServer {
3592
3877
  sessionIsEncrypted: !!sessionKey
3593
3878
  };
3594
3879
  this.pendingLaunchSessionId = null;
3880
+ try {
3881
+ await this.toolActivity?.start(sessionId);
3882
+ } catch (error) {
3883
+ logger.warn("[tool-activity] Agy consolidator unavailable; retaining raw TOOL_USE fallback", {
3884
+ sessionId,
3885
+ error: String(error)
3886
+ });
3887
+ }
3595
3888
  try {
3596
3889
  const stop = this.appSyncClient.subscribeToEvents(
3597
3890
  sessionId,
@@ -3709,20 +4002,36 @@ var McpServer = class _McpServer {
3709
4002
  this.approvalDetector.registerPendingCall(reg);
3710
4003
  }
3711
4004
  for (const input of mapped.events) {
3712
- if (input.type === import_codevibe_core4.EventType.TOOL_USE && typeof input.metadata === "object" && input.metadata) {
4005
+ if (input.type === import_codevibe_core5.EventType.USER_PROMPT || input.type === import_codevibe_core5.EventType.ASSISTANT_RESPONSE || input.type === import_codevibe_core5.EventType.NOTIFICATION) {
4006
+ await this.flushToolActivityAtBoundary(input.type);
4007
+ }
4008
+ if (input.type === import_codevibe_core5.EventType.TOOL_USE && typeof input.metadata === "object" && input.metadata) {
3713
4009
  const meta = input.metadata;
3714
4010
  const resultStep = typeof meta.step_index === "number" ? meta.step_index : void 0;
3715
4011
  const agyType = typeof meta.agy_type === "string" ? meta.agy_type : void 0;
3716
4012
  if (resultStep !== void 0 && agyType) {
3717
- this.approvalDetector.clearOnResultEvent({
4013
+ const matchedCall = this.approvalDetector.clearOnResultEvent({
3718
4014
  conversationId,
3719
4015
  resultStepIndex: resultStep,
3720
4016
  toolType: agyType,
3721
4017
  resultByteOffset: byteOffset
3722
4018
  });
4019
+ try {
4020
+ const disposition = await this.toolActivity?.observeResult(emit, matchedCall);
4021
+ if (disposition && disposition !== "closed") {
4022
+ continue;
4023
+ }
4024
+ } catch (error) {
4025
+ logger.warn("[tool-activity] Agy result observation failed; emitting raw TOOL_USE fallback", {
4026
+ sessionId: session.sessionId,
4027
+ conversationId,
4028
+ resultStep,
4029
+ error: String(error)
4030
+ });
4031
+ }
3723
4032
  }
3724
4033
  }
3725
- if (input.type === import_codevibe_core4.EventType.USER_PROMPT && input.source === import_codevibe_core4.EventSource.DESKTOP) {
4034
+ if (input.type === import_codevibe_core5.EventType.USER_PROMPT && input.source === import_codevibe_core5.EventSource.DESKTOP) {
3726
4035
  const clearedPromptIds = this.approvalDetector.onPanePromptCleared();
3727
4036
  await this.resolveAcceptedInputPrompts(session, clearedPromptIds);
3728
4037
  const dedupe = this.mobileDeduper.consumeIfDuplicate(session.sessionId, input.content);
@@ -3735,9 +4044,7 @@ var McpServer = class _McpServer {
3735
4044
  }
3736
4045
  }
3737
4046
  try {
3738
- const outbound = this.encryptOutbound(session, input);
3739
- if (!outbound) continue;
3740
- await this.appSyncClient.createEvent(outbound);
4047
+ if (!await this.createEncryptedEvent(session, input)) continue;
3741
4048
  } catch (err) {
3742
4049
  logger.error("createEvent failed", {
3743
4050
  sessionId: session.sessionId,
@@ -3747,6 +4054,67 @@ var McpServer = class _McpServer {
3747
4054
  }
3748
4055
  }
3749
4056
  }
4057
+ async flushToolActivityAtBoundary(boundary) {
4058
+ try {
4059
+ await this.toolActivity?.flush();
4060
+ } catch (error) {
4061
+ logger.warn("[tool-activity] Agy boundary flush failed; retaining individual event", {
4062
+ boundary,
4063
+ error: String(error)
4064
+ });
4065
+ }
4066
+ }
4067
+ async refreshToolActivitySessionKey(sessionId) {
4068
+ if (this.toolActivityKeyRefresh?.sessionId === sessionId) {
4069
+ return this.toolActivityKeyRefresh.promise;
4070
+ }
4071
+ const promise = this.loadAndInstallToolActivitySessionKey(sessionId);
4072
+ this.toolActivityKeyRefresh = { sessionId, promise };
4073
+ try {
4074
+ return await promise;
4075
+ } finally {
4076
+ if (this.toolActivityKeyRefresh?.promise === promise) {
4077
+ this.toolActivityKeyRefresh = null;
4078
+ }
4079
+ }
4080
+ }
4081
+ async loadAndInstallToolActivitySessionKey(sessionId) {
4082
+ const lifecycleGen = this.lifecycleGen;
4083
+ const expectedSession = this.session;
4084
+ try {
4085
+ const session = await this.appSyncClient.getSession(sessionId);
4086
+ if (session?.status === import_codevibe_core5.SessionStatus.INACTIVE) {
4087
+ const terminal = new Error(`Antigravity session is no longer active: ${sessionId}`);
4088
+ terminal.errorType = "SessionTerminal";
4089
+ throw terminal;
4090
+ }
4091
+ if (!session || session.status !== import_codevibe_core5.SessionStatus.ACTIVE || !session.isEncrypted) {
4092
+ return null;
4093
+ }
4094
+ const sessionKeyGen = session.sessionKeyGen ?? null;
4095
+ const encryptedKeys = session.encryptedKeys ?? [];
4096
+ if (encryptedKeys.length === 0) return null;
4097
+ const sessionKey = await import_codevibe_core5.keychainManager.getSessionKey(
4098
+ sessionId,
4099
+ encryptedKeys,
4100
+ sessionKeyGen
4101
+ );
4102
+ if (!sessionKey) return null;
4103
+ const liveSession = this.session;
4104
+ if (lifecycleGen !== this.lifecycleGen || !this.started || !expectedSession || liveSession !== expectedSession || liveSession.sessionId !== sessionId) return null;
4105
+ liveSession.sessionKey = sessionKey;
4106
+ liveSession.sessionKeyGen = sessionKeyGen;
4107
+ liveSession.sessionIsEncrypted = true;
4108
+ return { sessionKey, sessionKeyGen };
4109
+ } catch (error) {
4110
+ if ((0, import_codevibe_core5.isTerminalSessionError)(error)) throw error;
4111
+ logger.warn("[tool-activity] Agy session-key refresh failed; window remains retryable", {
4112
+ sessionId,
4113
+ error: String(error)
4114
+ });
4115
+ return null;
4116
+ }
4117
+ }
3750
4118
  /**
3751
4119
  * (#638) THE single encrypt-or-fail-closed decision for EVERY mobile-visible
3752
4120
  * emit, delegated to @quantiya/codevibe-core's encryptForEmit so all three
@@ -3759,7 +4127,7 @@ var McpServer = class _McpServer {
3759
4127
  * an encrypted keyless one (drop).
3760
4128
  */
3761
4129
  encryptOutbound(session, input) {
3762
- const enc = (0, import_codevibe_core4.encryptForEmit)(
4130
+ const enc = (0, import_codevibe_core5.encryptForEmit)(
3763
4131
  session.sessionKey ?? null,
3764
4132
  session.sessionIsEncrypted ?? false,
3765
4133
  input.content,
@@ -3779,9 +4147,28 @@ var McpServer = class _McpServer {
3779
4147
  ...input,
3780
4148
  content: enc.content,
3781
4149
  metadata: enc.metadata,
3782
- isEncrypted: true
4150
+ isEncrypted: true,
4151
+ ...session.sessionKeyGen ? { expectedSessionKeyGen: session.sessionKeyGen } : {}
3783
4152
  };
3784
4153
  }
4154
+ /** Encrypt and send one individual event, refreshing a stale session key once. */
4155
+ async createEncryptedEvent(session, input) {
4156
+ const lifecycleGen = this.lifecycleGen;
4157
+ const sendOnce = async () => {
4158
+ if (lifecycleGen !== this.lifecycleGen || !this.started || this.session !== session || input.sessionId !== session.sessionId) return null;
4159
+ const outbound = this.encryptOutbound(session, input);
4160
+ if (!outbound) return null;
4161
+ return this.appSyncClient.createEvent(outbound);
4162
+ };
4163
+ try {
4164
+ return await sendOnce();
4165
+ } catch (error) {
4166
+ if (!(0, import_codevibe_core5.isSessionKeyStaleError)(error)) throw error;
4167
+ const refreshed = await this.refreshToolActivitySessionKey(session.sessionId);
4168
+ if (!refreshed) throw error;
4169
+ return sendOnce();
4170
+ }
4171
+ }
3785
4172
  // ─── E1 missed-prompt recovery (§4/§6a) — in-memory ledger + raise helpers ───
3786
4173
  //
3787
4174
  // The authoritative ledger is the in-memory `e1PromptRaises` Map (see its field
@@ -3797,7 +4184,7 @@ var McpServer = class _McpServer {
3797
4184
  * NOTIFICATION carrier, no fabricated options).
3798
4185
  */
3799
4186
  newE1Raise(sessionId, mobileActionable, title) {
3800
- const { record } = (0, import_codevibe_core4.newPromptRaise)({
4187
+ const { record } = (0, import_codevibe_core5.newPromptRaise)({
3801
4188
  sessionId,
3802
4189
  producerKind: "PLUGIN_APPROVAL",
3803
4190
  mobileActionable,
@@ -3833,8 +4220,8 @@ var McpServer = class _McpServer {
3833
4220
  try {
3834
4221
  const ack = await this.appSyncClient.createEvent({
3835
4222
  sessionId: entry.record.sessionId || sessionId,
3836
- type: import_codevibe_core4.EventType.NOTIFICATION,
3837
- source: import_codevibe_core4.EventSource.DESKTOP,
4223
+ type: import_codevibe_core5.EventType.NOTIFICATION,
4224
+ source: import_codevibe_core5.EventSource.DESKTOP,
3838
4225
  resolvedPromptId: promptId,
3839
4226
  ownerToken: entry.record.ownerToken
3840
4227
  });
@@ -3895,17 +4282,16 @@ var McpServer = class _McpServer {
3895
4282
  const bannerText = "\u26A0\uFE0F A prompt is waiting in your desktop terminal, but its options could not be shown here. Please answer it on your desktop.";
3896
4283
  const input = {
3897
4284
  sessionId: session.sessionId,
3898
- type: import_codevibe_core4.EventType.NOTIFICATION,
3899
- source: import_codevibe_core4.EventSource.DESKTOP,
4285
+ type: import_codevibe_core5.EventType.NOTIFICATION,
4286
+ source: import_codevibe_core5.EventSource.DESKTOP,
3900
4287
  content: bannerText,
3901
4288
  metadata: { e1SuppressedPrompt: true },
3902
- ...(0, import_codevibe_core4.raiseFieldsFromRecord)(record),
4289
+ ...(0, import_codevibe_core5.raiseFieldsFromRecord)(record),
3903
4290
  notificationText: bannerText,
3904
- timestamp: (0, import_codevibe_core4.prepareEventTimestamp)({ orderingKey: session.sessionId })
4291
+ timestamp: (0, import_codevibe_core5.prepareEventTimestamp)({ orderingKey: session.sessionId })
3905
4292
  };
3906
- const outbound = this.encryptOutbound(session, input);
3907
- if (!outbound) return false;
3908
- await this.appSyncClient.createEvent(outbound);
4293
+ await this.flushToolActivityAtBoundary(import_codevibe_core5.EventType.NOTIFICATION);
4294
+ if (!await this.createEncryptedEvent(session, input)) return false;
3909
4295
  logger.info("E1: emitted suppressed-prompt badge carrier (mobileActionable=false)", {
3910
4296
  sessionId: session.sessionId,
3911
4297
  promptId: record.promptId
@@ -4072,16 +4458,15 @@ var McpServer = class _McpServer {
4072
4458
  if (actionable) {
4073
4459
  const input = {
4074
4460
  sessionId: session.sessionId,
4075
- type: import_codevibe_core4.EventType.INTERACTIVE_PROMPT,
4076
- source: import_codevibe_core4.EventSource.DESKTOP,
4461
+ type: import_codevibe_core5.EventType.INTERACTIVE_PROMPT,
4462
+ source: import_codevibe_core5.EventSource.DESKTOP,
4077
4463
  content: entry.contentPlain,
4078
4464
  metadata: entry.metadataPlain ?? {},
4079
- ...(0, import_codevibe_core4.raiseFieldsFromRecord)(rec),
4080
- timestamp: (0, import_codevibe_core4.prepareEventTimestamp)({ orderingKey: session.sessionId })
4465
+ ...(0, import_codevibe_core5.raiseFieldsFromRecord)(rec),
4466
+ timestamp: (0, import_codevibe_core5.prepareEventTimestamp)({ orderingKey: session.sessionId })
4081
4467
  };
4082
- const outbound = this.encryptOutbound(session, input);
4083
- if (!outbound) return false;
4084
- await this.appSyncClient.createEvent(outbound);
4468
+ await this.flushToolActivityAtBoundary(import_codevibe_core5.EventType.INTERACTIVE_PROMPT);
4469
+ if (!await this.createEncryptedEvent(session, input)) return false;
4085
4470
  } else {
4086
4471
  rec.mobileActionable = false;
4087
4472
  if (!await this.emitSuppressedPromptCarrier(session, rec)) return false;
@@ -4219,6 +4604,14 @@ var McpServer = class _McpServer {
4219
4604
  }
4220
4605
  this.subscription = null;
4221
4606
  }
4607
+ try {
4608
+ await this.toolActivity?.prepareForReplacement();
4609
+ } catch (error) {
4610
+ logger.warn("[tool-activity] Failed to settle retired Agy session", {
4611
+ sessionId: old.sessionId,
4612
+ error: String(error)
4613
+ });
4614
+ }
4222
4615
  this.session = null;
4223
4616
  await this.ensureLaunchSession();
4224
4617
  if (this.session) await this.drainE1RetirementPark();
@@ -4253,10 +4646,10 @@ var McpServer = class _McpServer {
4253
4646
  }
4254
4647
  const result = { ...evt };
4255
4648
  try {
4256
- result.content = import_codevibe_core4.cryptoService.decryptContent(evt.content, session.sessionKey);
4649
+ result.content = import_codevibe_core5.cryptoService.decryptContent(evt.content, session.sessionKey);
4257
4650
  const rawMeta = parseMaybeJson(evt.metadata);
4258
4651
  if (rawMeta && typeof rawMeta === "object" && typeof rawMeta.encrypted === "string") {
4259
- const decryptedMeta = import_codevibe_core4.cryptoService.decryptMetadata(
4652
+ const decryptedMeta = import_codevibe_core5.cryptoService.decryptMetadata(
4260
4653
  rawMeta.encrypted,
4261
4654
  session.sessionKey
4262
4655
  );
@@ -4335,6 +4728,7 @@ var McpServer = class _McpServer {
4335
4728
  });
4336
4729
  return;
4337
4730
  }
4731
+ await this.flushToolActivityAtBoundary(import_codevibe_core5.EventType.INTERACTIVE_PROMPT);
4338
4732
  const e1existing = this.e1PromptRaises.get(state.promptId);
4339
4733
  const e1rec = e1existing ? e1existing.record : this.newE1Raise(session.sessionId, true);
4340
4734
  if (!e1existing) state.promptId = e1rec.promptId;
@@ -4373,26 +4767,24 @@ var McpServer = class _McpServer {
4373
4767
  const content = state.paneDisplayHeader || state.matchedPaneHeader || state.pendingCall?.command || state.pendingCall?.filePath || "approval requested";
4374
4768
  const input = {
4375
4769
  sessionId: session.sessionId,
4376
- type: import_codevibe_core4.EventType.INTERACTIVE_PROMPT,
4377
- source: import_codevibe_core4.EventSource.DESKTOP,
4770
+ type: import_codevibe_core5.EventType.INTERACTIVE_PROMPT,
4771
+ source: import_codevibe_core5.EventSource.DESKTOP,
4378
4772
  content,
4379
4773
  metadata: optionsForMobile,
4380
4774
  // E1 (§4) — raise identity top-level: promptId + ownerToken/producerKind/
4381
4775
  // mobileActionable (the backend's classifyRaise upserts the open-prompt row).
4382
- ...(0, import_codevibe_core4.raiseFieldsFromRecord)(e1rec),
4776
+ ...(0, import_codevibe_core5.raiseFieldsFromRecord)(e1rec),
4383
4777
  // Per-conversation monotonic timestamp (event-timestamp-ordering fix
4384
4778
  // v5.4, 2026-05-24). orderingKey scopes the counter to this conv so
4385
4779
  // INTERACTIVE_PROMPT can't be forced to lastMs+1 by a newer event
4386
4780
  // from a sibling conversation under v9's multi-conv-per-session
4387
4781
  // model.
4388
- timestamp: (0, import_codevibe_core4.prepareEventTimestamp)({
4782
+ timestamp: (0, import_codevibe_core5.prepareEventTimestamp)({
4389
4783
  orderingKey: `${session.sessionId}:${state.conversationId}`
4390
4784
  })
4391
4785
  };
4392
4786
  try {
4393
- const outbound = this.encryptOutbound(session, input);
4394
- if (!outbound) return;
4395
- await this.appSyncClient.createEvent(outbound);
4787
+ if (!await this.createEncryptedEvent(session, input)) return;
4396
4788
  this.stashE1RaiseContent(
4397
4789
  e1rec.promptId,
4398
4790
  content,
@@ -4443,7 +4835,7 @@ var McpServer = class _McpServer {
4443
4835
  if (shouldDecrypt && sessionKey) {
4444
4836
  try {
4445
4837
  logger.info("Decrypting attachment", { id: attachment.id });
4446
- buffer = import_codevibe_core4.cryptoService.decryptData(buffer, sessionKey);
4838
+ buffer = import_codevibe_core5.cryptoService.decryptData(buffer, sessionKey);
4447
4839
  logger.info("Attachment decrypted successfully", {
4448
4840
  id: attachment.id,
4449
4841
  decryptedSize: buffer.length
@@ -4498,7 +4890,7 @@ var McpServer = class _McpServer {
4498
4890
  await this.ensureLaunchSession();
4499
4891
  if (!this.session) return;
4500
4892
  const session = this.session;
4501
- if (evt.source !== import_codevibe_core4.EventSource.MOBILE) return;
4893
+ if (evt.source !== import_codevibe_core5.EventSource.MOBILE) return;
4502
4894
  if (evt.sessionId !== session.sessionId) {
4503
4895
  logger.warn("Dropping mobile event with mismatched sessionId", {
4504
4896
  expected: session.sessionId,
@@ -4514,14 +4906,14 @@ var McpServer = class _McpServer {
4514
4906
  }
4515
4907
  evt = decrypted;
4516
4908
  await this.markDelivered(evt);
4517
- if (evt.type === import_codevibe_core4.EventType.USER_PROMPT || evt.type === import_codevibe_core4.EventType.PROMPT_RESPONSE) {
4909
+ if (evt.type === import_codevibe_core5.EventType.USER_PROMPT || evt.type === import_codevibe_core5.EventType.PROMPT_RESPONSE) {
4518
4910
  const mobileFloorMs = Date.parse(evt.timestamp);
4519
4911
  this.mobilePromptFloorBySession.set(session.sessionId, {
4520
4912
  floorMs: Number.isNaN(mobileFloorMs) ? Date.now() : mobileFloorMs,
4521
4913
  setAtWallMs: Date.now()
4522
4914
  });
4523
4915
  }
4524
- if (evt.type === import_codevibe_core4.EventType.USER_PROMPT || evt.type === import_codevibe_core4.EventType.PROMPT_RESPONSE) {
4916
+ if (evt.type === import_codevibe_core5.EventType.USER_PROMPT || evt.type === import_codevibe_core5.EventType.PROMPT_RESPONSE) {
4525
4917
  let promptContent = evt.content;
4526
4918
  const attachments = evt.attachments ?? [];
4527
4919
  if (attachments.length > 0) {
@@ -4560,16 +4952,16 @@ var McpServer = class _McpServer {
4560
4952
  const rawMeta = parseMaybeJson(evt.metadata);
4561
4953
  const meta = rawMeta && typeof rawMeta === "object" ? rawMeta : {};
4562
4954
  const evtAny = evt;
4563
- const responsePromptId = evt.type === import_codevibe_core4.EventType.PROMPT_RESPONSE ? typeof meta.promptId === "string" && meta.promptId || typeof meta.prompt_id === "string" && meta.prompt_id || typeof evtAny.promptId === "string" && evtAny.promptId || typeof evtAny.prompt_id === "string" && evtAny.prompt_id || null : null;
4955
+ const responsePromptId = evt.type === import_codevibe_core5.EventType.PROMPT_RESPONSE ? typeof meta.promptId === "string" && meta.promptId || typeof meta.prompt_id === "string" && meta.prompt_id || typeof evtAny.promptId === "string" && evtAny.promptId || typeof evtAny.prompt_id === "string" && evtAny.prompt_id || null : null;
4564
4956
  const advertisedOptionNumbers = Array.from(new Set(
4565
4957
  pendingPrompts.flatMap((prompt) => Object.keys(prompt.submitMap))
4566
4958
  ));
4567
- const intent = (0, import_codevibe_core4.classifyMobilePromptInput)(
4959
+ const intent = (0, import_codevibe_core5.classifyMobilePromptInput)(
4568
4960
  promptContent,
4569
4961
  advertisedOptionNumbers
4570
4962
  );
4571
4963
  if (intent.kind === "option") {
4572
- const activePrompt = evt.type === import_codevibe_core4.EventType.PROMPT_RESPONSE ? pendingPrompts.find((prompt) => prompt.promptId === responsePromptId) ?? null : pendingPrompts.length === 1 ? pendingPrompts[0] : null;
4964
+ const activePrompt = evt.type === import_codevibe_core5.EventType.PROMPT_RESPONSE ? pendingPrompts.find((prompt) => prompt.promptId === responsePromptId) ?? null : pendingPrompts.length === 1 ? pendingPrompts[0] : null;
4573
4965
  if (!activePrompt || !Object.prototype.hasOwnProperty.call(activePrompt.submitMap, intent.option)) {
4574
4966
  logger.warn("Numeric input does not identify one current advertised prompt; dropping", {
4575
4967
  sessionId: session.sessionId,
@@ -4579,7 +4971,7 @@ var McpServer = class _McpServer {
4579
4971
  });
4580
4972
  return;
4581
4973
  }
4582
- if (evt.type === import_codevibe_core4.EventType.PROMPT_RESPONSE) {
4974
+ if (evt.type === import_codevibe_core5.EventType.PROMPT_RESPONSE) {
4583
4975
  if (responsePromptId !== activePrompt.promptId) {
4584
4976
  logger.warn("PROMPT_RESPONSE does not identify the current advertised prompt; dropping", {
4585
4977
  sessionId: session.sessionId,
@@ -4631,16 +5023,15 @@ var McpServer = class _McpServer {
4631
5023
  async emitPromptSafetyNotification(session, content) {
4632
5024
  const input = {
4633
5025
  sessionId: session.sessionId,
4634
- type: import_codevibe_core4.EventType.NOTIFICATION,
4635
- source: import_codevibe_core4.EventSource.DESKTOP,
5026
+ type: import_codevibe_core5.EventType.NOTIFICATION,
5027
+ source: import_codevibe_core5.EventSource.DESKTOP,
4636
5028
  content,
4637
5029
  metadata: { promptSafetyBlocked: true },
4638
- timestamp: (0, import_codevibe_core4.prepareEventTimestamp)({ orderingKey: session.sessionId })
5030
+ timestamp: (0, import_codevibe_core5.prepareEventTimestamp)({ orderingKey: session.sessionId })
4639
5031
  };
4640
- const outbound = this.encryptOutbound(session, input);
4641
- if (!outbound) return;
5032
+ await this.flushToolActivityAtBoundary(import_codevibe_core5.EventType.NOTIFICATION);
4642
5033
  try {
4643
- await this.appSyncClient.createEvent(outbound);
5034
+ await this.createEncryptedEvent(session, input);
4644
5035
  } catch (error) {
4645
5036
  logger.warn("Failed to emit prompt-safety notification", { error: String(error) });
4646
5037
  }
@@ -4659,7 +5050,7 @@ var McpServer = class _McpServer {
4659
5050
  eventId: evt.eventId,
4660
5051
  sessionId: evt.sessionId,
4661
5052
  timestamp: evt.timestamp,
4662
- deliveryStatus: import_codevibe_core4.DeliveryStatus.DELIVERED
5053
+ deliveryStatus: import_codevibe_core5.DeliveryStatus.DELIVERED
4663
5054
  });
4664
5055
  } catch (err) {
4665
5056
  logger.warn("updateEventStatus(DELIVERED) failed", {
@@ -4683,7 +5074,7 @@ var McpServer = class _McpServer {
4683
5074
  eventId: evt.eventId,
4684
5075
  sessionId: evt.sessionId,
4685
5076
  timestamp: evt.timestamp,
4686
- deliveryStatus: import_codevibe_core4.DeliveryStatus.EXECUTED
5077
+ deliveryStatus: import_codevibe_core5.DeliveryStatus.EXECUTED
4687
5078
  });
4688
5079
  } catch (err) {
4689
5080
  logger.warn("updateEventStatus(EXECUTED) failed", {
@@ -4711,15 +5102,14 @@ var McpServer = class _McpServer {
4711
5102
  // orderingKey is session-scoped. Slightly coarser than the
4712
5103
  // per-conv key used by transcript emits, but acceptable because
4713
5104
  // hook-script pushes are off the hot transcript path.
4714
- timestamp: (0, import_codevibe_core4.prepareEventTimestamp)({ orderingKey: payload.sessionId })
5105
+ timestamp: (0, import_codevibe_core5.prepareEventTimestamp)({ orderingKey: payload.sessionId })
4715
5106
  };
4716
- const finalInput = this.encryptOutbound(session, input);
4717
- if (!finalInput) {
5107
+ const created = await this.createEncryptedEvent(session, input);
5108
+ if (!created) {
4718
5109
  throw new Error(
4719
5110
  `fail-closed: encrypted session ${payload.sessionId} has no session key \u2014 event dropped (#638)`
4720
5111
  );
4721
5112
  }
4722
- const created = await this.appSyncClient.createEvent(finalInput);
4723
5113
  return { eventId: created.eventId };
4724
5114
  }
4725
5115
  findSessionByBackendId(backendSessionId) {
@@ -4950,7 +5340,7 @@ async function main() {
4950
5340
  tmuxTarget: process.env.CODEVIBE_AGY_TMUX_TARGET ?? void 0,
4951
5341
  wrapperPid: process.env.CODEVIBE_AGY_WRAPPER_PID ? parseInt(process.env.CODEVIBE_AGY_WRAPPER_PID, 10) : void 0
4952
5342
  });
4953
- (0, import_codevibe_core4.installDaemonProcessGuards)(logger, {
5343
+ (0, import_codevibe_core5.installDaemonProcessGuards)(logger, {
4954
5344
  onFatal: () => {
4955
5345
  void server.stop().finally(() => process.exit(1));
4956
5346
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quantiya/codevibe-antigravity-plugin",
3
- "version": "2.0.17",
3
+ "version": "2.0.19",
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": {
@@ -48,7 +48,7 @@
48
48
  "node": ">=22.0.0"
49
49
  },
50
50
  "dependencies": {
51
- "@quantiya/codevibe-core": "2.0.16",
51
+ "@quantiya/codevibe-core": "2.0.17",
52
52
  "chokidar": "^5.0.0",
53
53
  "dotenv": "^16.6.1",
54
54
  "express": "^5.1.0",