@quantiya/codevibe-antigravity-plugin 2.0.61 → 2.0.62

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 +516 -169
  2. package/package.json +2 -2
package/dist/server.js CHANGED
@@ -49,7 +49,7 @@ var fs5 = __toESM(require("fs"));
49
49
  var os5 = __toESM(require("os"));
50
50
  var import_child_process3 = require("child_process");
51
51
  var import_util3 = require("util");
52
- var import_codevibe_core5 = require("@quantiya/codevibe-core");
52
+ var import_codevibe_core6 = require("@quantiya/codevibe-core");
53
53
 
54
54
  // src/logger.ts
55
55
  var import_os = __toESM(require("os"));
@@ -205,23 +205,20 @@ var HttpApi = class {
205
205
  this.sendJson(res, 400, { success: false, error: "invalid JSON" });
206
206
  return;
207
207
  }
208
- if (!payload.sessionId || !payload.type || !payload.source || typeof payload.content !== "string") {
209
- this.sendJson(res, 400, {
210
- success: false,
211
- error: "missing required fields: sessionId, type, source, content"
212
- });
213
- return;
214
- }
215
- if (!isValidEventType(payload.type) || !isValidEventSource(payload.source)) {
208
+ if (!isHttpEventPayload(payload)) {
216
209
  this.sendJson(res, 400, {
217
210
  success: false,
218
- error: "invalid type or source enum value"
211
+ error: "invalid ordinary desktop event payload"
219
212
  });
220
213
  return;
221
214
  }
222
215
  try {
223
- const { eventId } = await this.handlers.pushEvent(payload);
224
- this.sendJson(res, 200, { success: true, data: { eventId } });
216
+ const result = await this.handlers.pushEvent(payload);
217
+ if (result.status === "DROPPED") {
218
+ this.sendJson(res, 503, { success: false, error: "event was not admitted", data: result });
219
+ } else {
220
+ this.sendJson(res, result.status === "PENDING" ? 202 : 200, { success: true, data: result });
221
+ }
225
222
  } catch (err) {
226
223
  logger.error("pushEvent failed in /event handler", { error: String(err) });
227
224
  const code = err?.code;
@@ -229,6 +226,10 @@ var HttpApi = class {
229
226
  this.sendJson(res, 404, { success: false, error: "session not found" });
230
227
  return;
231
228
  }
229
+ if (code === "INVALID_SYNC_INPUT") {
230
+ this.sendJson(res, 400, { success: false, error: "unsupported event payload; use its dedicated producer" });
231
+ return;
232
+ }
232
233
  this.sendJson(res, 500, { success: false, error: "event push failed" });
233
234
  }
234
235
  }
@@ -263,9 +264,10 @@ function isValidEventType(value) {
263
264
  if (typeof value !== "string") return false;
264
265
  return Object.values(import_codevibe_core2.EventType).includes(value);
265
266
  }
266
- function isValidEventSource(value) {
267
- if (typeof value !== "string") return false;
268
- return Object.values(import_codevibe_core2.EventSource).includes(value);
267
+ function isHttpEventPayload(value) {
268
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
269
+ const row = value;
270
+ return !Object.keys(row).some((key) => !["sessionId", "type", "source", "content", "metadata"].includes(key)) && typeof row.sessionId === "string" && /^[A-Za-z0-9_-]{1,256}$/.test(row.sessionId) && isValidEventType(row.type) && row.source === import_codevibe_core2.EventSource.DESKTOP && typeof row.content === "string";
269
271
  }
270
272
  async function readBody(req, maxBytes) {
271
273
  return new Promise((resolve2, reject) => {
@@ -346,6 +348,29 @@ function armShutdownWatchdog(exit, onFire, ms = SHUTDOWN_WATCHDOG_MS, durableObl
346
348
  return () => clearTimeout(timer);
347
349
  }
348
350
 
351
+ // src/durable-transcript-reader.ts
352
+ var import_codevibe_core3 = require("@quantiya/codevibe-core");
353
+ function readAdmittedTranscript(options) {
354
+ const stream = (value) => Object.freeze({ ...value, conversationId: options.conversationId });
355
+ return (0, import_codevibe_core3.readDesktopSyncFile)({
356
+ ...options,
357
+ parse: (line) => {
358
+ const parsed = JSON.parse(line);
359
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) && Number.isFinite(parsed.step_index) && typeof parsed.type === "string" ? parsed : null;
360
+ },
361
+ admission: {
362
+ restore: (source, initial) => options.admission.restore(stream(source), initial),
363
+ skip: (gap, source) => options.admission.skip(gap, stream(source)),
364
+ admit: (event, source) => options.admission.admit({
365
+ conversationId: options.conversationId,
366
+ filePath: source.filePath,
367
+ byteOffset: source.startOffset,
368
+ event
369
+ }, stream(source))
370
+ }
371
+ });
372
+ }
373
+
349
374
  // src/transcript-tailer.ts
350
375
  var OFFSET_FLUSH_INTERVAL_MS = 3e4;
351
376
  var ERROR_LOG_MIN_INTERVAL_MS = 1e4;
@@ -391,6 +416,8 @@ var TranscriptTailer = class extends import_events.EventEmitter {
391
416
  super();
392
417
  this.isWatching = false;
393
418
  this.isShuttingDown = false;
419
+ this.ownedReads = /* @__PURE__ */ new Set();
420
+ this.retryTimers = /* @__PURE__ */ new Map();
394
421
  /** Transcripts currently followed (stat-polled), keyed by conversation. */
395
422
  this.followed = /* @__PURE__ */ new Map();
396
423
  /** Conversations released by the follow cap during this lifetime. Their
@@ -448,6 +475,8 @@ var TranscriptTailer = class extends import_events.EventEmitter {
448
475
  const defaults = defaultPaths();
449
476
  this.brainRoot = options.brainRoot ?? defaults.brainRoot;
450
477
  this.offsetFilePath = options.offsetFilePath ?? defaults.offsetFilePath;
478
+ this.admission = options.admission;
479
+ if (this.admission && (typeof this.admission.restore !== "function" || typeof this.admission.admit !== "function" || typeof this.admission.skip !== "function")) throw new TypeError("Durable transcript admission requires restore, admit and skip");
451
480
  }
452
481
  /**
453
482
  * Start following transcripts. Loads persisted offsets, prunes orphans,
@@ -461,6 +490,8 @@ var TranscriptTailer = class extends import_events.EventEmitter {
461
490
  }
462
491
  this.startTime = Date.now();
463
492
  this.readGeneration++;
493
+ for (const timer of this.retryTimers.values()) clearTimeout(timer);
494
+ this.retryTimers.clear();
464
495
  this.loadPersistedOffsets();
465
496
  this.pruneOrphanedOffsets();
466
497
  if (!fs2.existsSync(this.brainRoot)) {
@@ -542,6 +573,8 @@ var TranscriptTailer = class extends import_events.EventEmitter {
542
573
  if (!this.isWatching) return;
543
574
  this.isShuttingDown = true;
544
575
  this.readGeneration++;
576
+ for (const timer of this.retryTimers.values()) clearTimeout(timer);
577
+ this.retryTimers.clear();
545
578
  if (this.flushTimer) {
546
579
  clearInterval(this.flushTimer);
547
580
  this.flushTimer = null;
@@ -584,6 +617,10 @@ var TranscriptTailer = class extends import_events.EventEmitter {
584
617
  this.lastErrorSignature = null;
585
618
  this.suppressedErrorCount = 0;
586
619
  }
620
+ /** Server shutdown joins reads it admitted before retiring the session. */
621
+ async waitForReads() {
622
+ await Promise.allSettled([...this.ownedReads]);
623
+ }
587
624
  /**
588
625
  * Best-effort sync flush. Safe to call from a process EXIT trap.
589
626
  * (The async stop() above is preferred when a regular shutdown is feasible.)
@@ -1052,9 +1089,19 @@ var TranscriptTailer = class extends import_events.EventEmitter {
1052
1089
  conversationId,
1053
1090
  error: String(error)
1054
1091
  });
1092
+ if (this.admission && this.isWatching && generation === this.readGeneration && !this.retryTimers.has(filePath)) {
1093
+ const timer = setTimeout(() => {
1094
+ this.retryTimers.delete(filePath);
1095
+ if (this.isWatching && generation === this.readGeneration) void this.scheduleReadNewLines(filePath, conversationId);
1096
+ }, 5e3);
1097
+ timer.unref();
1098
+ this.retryTimers.set(filePath, timer);
1099
+ }
1055
1100
  });
1056
1101
  this.activeReads.set(filePath, read);
1102
+ this.ownedReads.add(read);
1057
1103
  void read.finally(() => {
1104
+ this.ownedReads.delete(read);
1058
1105
  if (this.activeReads.get(filePath) === read) {
1059
1106
  this.activeReads.delete(filePath);
1060
1107
  this.pendingReads.delete(filePath);
@@ -1076,6 +1123,23 @@ var TranscriptTailer = class extends import_events.EventEmitter {
1076
1123
  logger.warn("No offset entry \u2014 skipping read", { filePath });
1077
1124
  return;
1078
1125
  }
1126
+ if (this.admission) {
1127
+ const result = await readAdmittedTranscript({
1128
+ filePath,
1129
+ conversationId,
1130
+ initialOffset: entry.byteOffset,
1131
+ admission: this.admission,
1132
+ isCurrent: () => generation === this.readGeneration && !this.isShuttingDown
1133
+ });
1134
+ if (result) {
1135
+ if (generation !== this.readGeneration) return;
1136
+ entry.byteOffset = result.position.byteOffset;
1137
+ entry.carryover = "";
1138
+ entry.lastKnownSize = result.size;
1139
+ entry.lastModifiedMs = result.mtimeMs;
1140
+ return;
1141
+ }
1142
+ }
1079
1143
  let stat;
1080
1144
  try {
1081
1145
  stat = fs2.statSync(filePath);
@@ -3152,7 +3216,7 @@ var PromptResponder = class {
3152
3216
  };
3153
3217
  }
3154
3218
  }
3155
- async sendFreeFormWhenNoApprovalActive(text) {
3219
+ async sendFreeFormWhenNoApprovalActive(text, beforeType) {
3156
3220
  const target = this.resolveTmuxTarget();
3157
3221
  if (!target) return { ok: false, reason: "no-tmux-target" };
3158
3222
  if (!this.paneObserver) {
@@ -3172,9 +3236,20 @@ var PromptResponder = class {
3172
3236
  }
3173
3237
  return null;
3174
3238
  };
3239
+ let preparationFailed = false;
3175
3240
  try {
3176
3241
  const beforeText = await chooserIsAbsent();
3177
3242
  if (beforeText) return beforeText;
3243
+ if (beforeType) {
3244
+ try {
3245
+ await beforeType();
3246
+ } catch (error) {
3247
+ preparationFailed = true;
3248
+ throw error;
3249
+ }
3250
+ const afterPreparation = await chooserIsAbsent();
3251
+ if (afterPreparation) return afterPreparation;
3252
+ }
3178
3253
  await this.typeLiteral(target, text);
3179
3254
  await delay(ENTER_DELAY_MS);
3180
3255
  const beforeEnter = await chooserIsAbsent();
@@ -3182,6 +3257,7 @@ var PromptResponder = class {
3182
3257
  await this.sendKey(target, "Enter");
3183
3258
  return { ok: true };
3184
3259
  } catch (error) {
3260
+ if (preparationFailed) throw error;
3185
3261
  return {
3186
3262
  ok: false,
3187
3263
  reason: "tmux-failed",
@@ -3314,6 +3390,16 @@ var MobilePromptDeduper = class {
3314
3390
  this.replaceEntries(sessionId, entries);
3315
3391
  return match;
3316
3392
  }
3393
+ /** Inspect without spending the token before durable native admission. */
3394
+ peekIfDuplicate(sessionId, echo) {
3395
+ const normalized = this.normalize(echo);
3396
+ if (!normalized) return null;
3397
+ for (const entry of this.validEntries(sessionId)) {
3398
+ const matchType = this.matchType(entry.normalized, normalized);
3399
+ if (matchType) return { matchType, originalLength: entry.normalized.length, echoLength: normalized.length };
3400
+ }
3401
+ return null;
3402
+ }
3317
3403
  // ─── Internals ──────────────────────────────────────────────────────────
3318
3404
  sweep() {
3319
3405
  const cutoff = this.now() - this.expiryMs;
@@ -3470,7 +3556,7 @@ function fireDaemonBeacon(name, params) {
3470
3556
 
3471
3557
  // src/event-mapper.ts
3472
3558
  var import_uuid2 = require("uuid");
3473
- var import_codevibe_core3 = require("@quantiya/codevibe-core");
3559
+ var import_codevibe_core4 = require("@quantiya/codevibe-core");
3474
3560
  var CONTENT_MAX = 150 * 1024;
3475
3561
  var TOOL_OUTPUT_MAX = 50 * 1024;
3476
3562
  var TOOL_DIFF_MAX = 100 * 1024;
@@ -3478,10 +3564,10 @@ function mapTranscriptEvent(emit, sessionId, notBeforeMs) {
3478
3564
  const { event } = emit;
3479
3565
  const base = {
3480
3566
  sessionId,
3481
- source: import_codevibe_core3.EventSource.DESKTOP
3567
+ source: import_codevibe_core4.EventSource.DESKTOP
3482
3568
  };
3483
3569
  const orderingKey = `${sessionId}:${emit.conversationId}`;
3484
- const nextTimestamp = () => (0, import_codevibe_core3.prepareEventTimestamp)({ orderingKey, agentClock: event.created_at, notBeforeMs });
3570
+ const nextTimestamp = () => (0, import_codevibe_core4.prepareEventTimestamp)({ orderingKey, agentClock: event.created_at, notBeforeMs });
3485
3571
  const baseMetadata = {
3486
3572
  step_index: event.step_index,
3487
3573
  agy_source: event.source,
@@ -3521,7 +3607,7 @@ function mapTranscriptEvent(emit, sessionId, notBeforeMs) {
3521
3607
  {
3522
3608
  ...base,
3523
3609
  timestamp: nextTimestamp(),
3524
- type: import_codevibe_core3.EventType.USER_PROMPT,
3610
+ type: import_codevibe_core4.EventType.USER_PROMPT,
3525
3611
  content: truncate(cleaned, CONTENT_MAX),
3526
3612
  metadata: { ...baseMetadata }
3527
3613
  }
@@ -3536,7 +3622,7 @@ function mapTranscriptEvent(emit, sessionId, notBeforeMs) {
3536
3622
  events.push({
3537
3623
  ...base,
3538
3624
  timestamp: nextTimestamp(),
3539
- type: import_codevibe_core3.EventType.REASONING,
3625
+ type: import_codevibe_core4.EventType.REASONING,
3540
3626
  content: truncate(event.thinking, CONTENT_MAX),
3541
3627
  metadata: { ...baseMetadata }
3542
3628
  });
@@ -3545,7 +3631,7 @@ function mapTranscriptEvent(emit, sessionId, notBeforeMs) {
3545
3631
  events.push({
3546
3632
  ...base,
3547
3633
  timestamp: nextTimestamp(),
3548
- type: import_codevibe_core3.EventType.ASSISTANT_RESPONSE,
3634
+ type: import_codevibe_core4.EventType.ASSISTANT_RESPONSE,
3549
3635
  content: truncate(event.content, CONTENT_MAX),
3550
3636
  metadata: { ...baseMetadata }
3551
3637
  });
@@ -3582,7 +3668,7 @@ function mapTranscriptEvent(emit, sessionId, notBeforeMs) {
3582
3668
  {
3583
3669
  ...base,
3584
3670
  timestamp: nextTimestamp(),
3585
- type: import_codevibe_core3.EventType.NOTIFICATION,
3671
+ type: import_codevibe_core4.EventType.NOTIFICATION,
3586
3672
  content: truncate(content, CONTENT_MAX),
3587
3673
  metadata: {
3588
3674
  ...baseMetadata,
@@ -3602,7 +3688,7 @@ function mapTranscriptEvent(emit, sessionId, notBeforeMs) {
3602
3688
  {
3603
3689
  ...base,
3604
3690
  timestamp: nextTimestamp(),
3605
- type: import_codevibe_core3.EventType.TOOL_USE,
3691
+ type: import_codevibe_core4.EventType.TOOL_USE,
3606
3692
  content: formatToolResultContent(event.type, toolName, truncatedContent, isErrorStatus),
3607
3693
  metadata: {
3608
3694
  ...baseMetadata,
@@ -3760,7 +3846,7 @@ function truncate(s, maxBytes) {
3760
3846
  }
3761
3847
 
3762
3848
  // src/tool-activity-integration.ts
3763
- var import_codevibe_core4 = require("@quantiya/codevibe-core");
3849
+ var import_codevibe_core5 = require("@quantiya/codevibe-core");
3764
3850
  var TOOL_ACTIVITY_CONTENT = "tool_activity";
3765
3851
  var DEFAULT_WINDOW_MS = 1e4;
3766
3852
  var DEFAULT_MAX_OCCURRENCES = 50;
@@ -3827,9 +3913,9 @@ function agyOccurrenceId(emit, matchedCall) {
3827
3913
  createdAt: emit.event.created_at,
3828
3914
  stepIndex: emit.event.step_index,
3829
3915
  type: emit.event.type,
3830
- contentDigest: (0, import_codevibe_core4.digestOf)(emit.event.content ?? "")
3916
+ contentDigest: (0, import_codevibe_core5.digestOf)(emit.event.content ?? "")
3831
3917
  };
3832
- return `agy:${(0, import_codevibe_core4.digestOf)(identity).slice("sha256:".length)}`;
3918
+ return `agy:${(0, import_codevibe_core5.digestOf)(identity).slice("sha256:".length)}`;
3833
3919
  }
3834
3920
  var AgyToolActivityIntegration = class {
3835
3921
  constructor(deps) {
@@ -3838,31 +3924,34 @@ var AgyToolActivityIntegration = class {
3838
3924
  this.sessionsToRehome = /* @__PURE__ */ new Set();
3839
3925
  this.deps = deps;
3840
3926
  }
3841
- isActive() {
3842
- return this.consolidator !== null;
3927
+ isActive(sessionId) {
3928
+ return this.consolidator !== null && (sessionId === void 0 || this.backendSessionId === sessionId);
3843
3929
  }
3844
3930
  async start(sessionId) {
3845
3931
  if (this.consolidator && this.backendSessionId === sessionId) return;
3846
3932
  if (this.consolidator) await this.stop();
3847
- const outbox = new import_codevibe_core4.ToolActivityOutbox({
3933
+ const outbox = new import_codevibe_core5.ToolActivityOutbox({
3848
3934
  agent: "agy",
3849
3935
  logger: this.deps.logger,
3850
3936
  root: this.deps.root
3851
3937
  });
3852
- await this.rehomePendingWindows(outbox, sessionId);
3853
- const ledger = new import_codevibe_core4.ToolActivityLedger({
3938
+ const ledger = new import_codevibe_core5.ToolActivityLedger({
3854
3939
  agent: "agy",
3855
3940
  sessionId,
3856
3941
  logger: this.deps.logger,
3857
3942
  root: this.deps.root
3858
3943
  });
3859
- const consolidator = new import_codevibe_core4.ToolActivityConsolidator({
3944
+ await this.deps.recoverPending?.(sessionId, outbox, ledger);
3945
+ const legacyTransport = this.buildTransport();
3946
+ const transport = this.deps.selectTransport ? await this.deps.selectTransport(sessionId, legacyTransport) : legacyTransport;
3947
+ if (transport.protocol !== "SYNC_V2") await this.rehomePendingWindows(outbox, sessionId);
3948
+ const consolidator = new import_codevibe_core5.ToolActivityConsolidator({
3860
3949
  agent: "agy",
3861
3950
  sessionId,
3862
3951
  logger: this.deps.logger,
3863
3952
  outbox,
3864
3953
  ledger,
3865
- transport: this.buildTransport(),
3954
+ transport,
3866
3955
  windowMs: this.deps.windowMs ?? DEFAULT_WINDOW_MS,
3867
3956
  maxOccurrences: DEFAULT_MAX_OCCURRENCES
3868
3957
  });
@@ -3894,7 +3983,7 @@ var AgyToolActivityIntegration = class {
3894
3983
  ...target ? { normalizedTarget: target } : {},
3895
3984
  ts: emit.event.created_at,
3896
3985
  byteOffset: emit.byteOffset,
3897
- ...inputIdentity ? { digest: (0, import_codevibe_core4.digestOf)(inputIdentity) } : {}
3986
+ ...inputIdentity ? { digest: (0, import_codevibe_core5.digestOf)(inputIdentity) } : {}
3898
3987
  });
3899
3988
  }
3900
3989
  async flush() {
@@ -3944,7 +4033,8 @@ var AgyToolActivityIntegration = class {
3944
4033
  }
3945
4034
  }
3946
4035
  if (!eligible) continue;
3947
- for (const envelope of pending.valid) {
4036
+ const legacyWindows = pending.valid.filter((envelope) => envelope.syncSessionGenerationId === void 0);
4037
+ for (const envelope of legacyWindows) {
3948
4038
  const rehomed = { ...envelope, backendSessionId: replacementSessionId };
3949
4039
  await outbox.persist(replacementSessionId, rehomed);
3950
4040
  await outbox.prune(oldSessionId, envelope.clientEventId);
@@ -3953,7 +4043,7 @@ var AgyToolActivityIntegration = class {
3953
4043
  this.deps.logger.info("[tool-activity] Rehomed durable Agy windows", {
3954
4044
  oldSessionId,
3955
4045
  replacementSessionId,
3956
- count: pending.valid.length
4046
+ count: legacyWindows.length
3957
4047
  });
3958
4048
  }
3959
4049
  }
@@ -3963,7 +4053,7 @@ var AgyToolActivityIntegration = class {
3963
4053
  if (!session || session.sessionId !== sessionId) {
3964
4054
  throw terminalSessionError(sessionId);
3965
4055
  }
3966
- const encrypted = (0, import_codevibe_core4.encryptForEmit)(
4056
+ const encrypted = (0, import_codevibe_core5.encryptForEmit)(
3967
4057
  override?.sessionKey ?? session.sessionKey ?? null,
3968
4058
  session.sessionIsEncrypted ?? false,
3969
4059
  TOOL_ACTIVITY_CONTENT,
@@ -3975,8 +4065,8 @@ var AgyToolActivityIntegration = class {
3975
4065
  const sessionKeyGen = override?.sessionKeyGen ?? session.sessionKeyGen;
3976
4066
  const input = {
3977
4067
  sessionId,
3978
- type: import_codevibe_core4.EventType.TOOL_ACTIVITY,
3979
- source: import_codevibe_core4.EventSource.DESKTOP,
4068
+ type: import_codevibe_core5.EventType.TOOL_ACTIVITY,
4069
+ source: import_codevibe_core5.EventSource.DESKTOP,
3980
4070
  content: encrypted.content,
3981
4071
  metadata: encrypted.metadata,
3982
4072
  timestamp: envelope.windowStart,
@@ -3991,10 +4081,10 @@ var AgyToolActivityIntegration = class {
3991
4081
  try {
3992
4082
  await sendOnce(sessionId, envelope);
3993
4083
  } catch (error) {
3994
- if ((0, import_codevibe_core4.isTerminalSessionError)(error)) {
4084
+ if ((0, import_codevibe_core5.isTerminalSessionError)(error)) {
3995
4085
  throw new Error(`[tool-activity] Agy session ${sessionId} retired; awaiting replacement`);
3996
4086
  }
3997
- if ((0, import_codevibe_core4.isSessionKeyStaleError)(error) && this.deps.refreshSessionKey) {
4087
+ if ((0, import_codevibe_core5.isSessionKeyStaleError)(error) && this.deps.refreshSessionKey) {
3998
4088
  const refreshed = await this.deps.refreshSessionKey(sessionId);
3999
4089
  if (refreshed) {
4000
4090
  await sendOnce(sessionId, envelope, refreshed);
@@ -4056,8 +4146,12 @@ var McpServer = class _McpServer {
4056
4146
  * under this single sessionId. /resume does NOT create a new
4057
4147
  * session — same row. */
4058
4148
  this.session = null;
4059
- this.promptPublications = new import_codevibe_core5.PromptPublicationTracker();
4149
+ this.promptPublications = new import_codevibe_core6.PromptPublicationTracker();
4060
4150
  this.transcriptHandlers = /* @__PURE__ */ new Set();
4151
+ this.nativeSourceSelection = null;
4152
+ this.nativeSources = /* @__PURE__ */ new Map();
4153
+ this.nativeToolStart = null;
4154
+ this.nativeAdmissionChain = Promise.resolve();
4061
4155
  this.toolActivityKeyRefresh = null;
4062
4156
  // E1 (§4/§6a-4) — per-promptId durable raise entries for the agy producer.
4063
4157
  // Keyed by the E1 promptId (= derive(ownerToken)); each holds the SECRET
@@ -4210,9 +4304,23 @@ var McpServer = class _McpServer {
4210
4304
  this.wrapperPid = options.wrapperPid ?? null;
4211
4305
  this.cliLogPath = options.cliLogPath ?? null;
4212
4306
  this.launchSettleTimeoutMs = options.launchSettleTimeoutMs ?? LAUNCH_SETTLE_TIMEOUT_MS;
4213
- this.appSyncClient = options.appSyncClient ?? new import_codevibe_core5.AppSyncClient();
4307
+ this.appSyncClient = options.appSyncClient ?? new import_codevibe_core6.AppSyncClient();
4214
4308
  this.toolActivity = options.toolActivityIntegration === void 0 ? new AgyToolActivityIntegration({
4215
4309
  logger,
4310
+ selectTransport: async (sessionId, legacyTransport) => {
4311
+ const session = this.session;
4312
+ if (!session || session.sessionId !== sessionId) throw Error("SYNC_SOURCE_OWNER_CHANGED");
4313
+ const selection = await this.selectNativeSource(session);
4314
+ if (this.session !== session || !this.started) throw Error("SYNC_SOURCE_OWNER_CHANGED");
4315
+ return selection.protocol === "LEGACY" ? legacyTransport : this.appSyncClient.toolActivitySyncTransport({
4316
+ sessionScope: selection.sessionScope,
4317
+ getEncryptionContext: selection.getEncryptionContext
4318
+ });
4319
+ },
4320
+ recoverPending: async (sessionId, outbox, ledger) => {
4321
+ await this.appSyncClient.recoverCompanionSyncSources({ sessionId });
4322
+ await this.appSyncClient.recoverToolActivityWindows({ sessionId, outbox, ledger });
4323
+ },
4216
4324
  createEvent: (input) => this.appSyncClient.createEvent(input),
4217
4325
  getSession: () => this.session,
4218
4326
  refreshSessionKey: (sessionId) => this.refreshToolActivitySessionKey(sessionId),
@@ -4228,7 +4336,11 @@ var McpServer = class _McpServer {
4228
4336
  this.workingDirectory = process.cwd();
4229
4337
  this.approvalDetector = options.approvalDetector ?? new ApprovalDetector(options.detectorOptions);
4230
4338
  this.paneObserver = options.paneObserver ?? new TmuxPaneObserver();
4231
- this.transcriptTailer = options.transcriptTailer ?? new TranscriptTailer();
4339
+ this.transcriptTailer = options.transcriptTailer ?? new TranscriptTailer({ admission: {
4340
+ restore: (source, initial) => this.restoreNativeSource(source, initial),
4341
+ admit: (emit, source) => this.admitNativeSource(emit, source),
4342
+ skip: (gap, source) => this.skipNativeSource(gap, source)
4343
+ } });
4232
4344
  this.mobileDeduper = options.mobileDeduper ?? new MobilePromptDeduper();
4233
4345
  this.promptResponder = options.promptResponder ?? new PromptResponder({
4234
4346
  tmuxTarget: this.tmuxTarget ?? void 0,
@@ -4263,7 +4375,7 @@ var McpServer = class _McpServer {
4263
4375
  const authed = await this.appSyncClient.authenticateWithStoredTokens();
4264
4376
  if (!authed) {
4265
4377
  const kind = this.appSyncClient.getLastAuthFailureKind();
4266
- const failure = (0, import_codevibe_core5.describeStoredAuthFailure)(
4378
+ const failure = (0, import_codevibe_core6.describeStoredAuthFailure)(
4267
4379
  kind,
4268
4380
  this.appSyncClient.getLastAuthFailureDetail()
4269
4381
  );
@@ -4282,9 +4394,32 @@ var McpServer = class _McpServer {
4282
4394
  this.stopRequestedDuringCreate = false;
4283
4395
  this.registerSignalHandlers();
4284
4396
  this.startTmuxLifecycleMonitor();
4285
- await (0, import_codevibe_core5.registerDeviceEncryptionKey)(this.appSyncClient, logger);
4397
+ try {
4398
+ const statusUserId = this.appSyncClient.getCurrentUserId();
4399
+ const signal = AbortSignal.timeout(5e3);
4400
+ const timeout = new Promise((_, reject) => signal.addEventListener(
4401
+ "abort",
4402
+ () => reject(new Error("status read timed out")),
4403
+ { once: true }
4404
+ ));
4405
+ const subscription = await Promise.race([this.appSyncClient.getSubscriptionStatus(signal), timeout]);
4406
+ if (this.appSyncClient.getCurrentUserId() !== statusUserId) throw new Error("account changed during read");
4407
+ const sync = await Promise.race([this.appSyncClient.getCompanionSyncState(signal), timeout]);
4408
+ if (this.appSyncClient.getCurrentUserId() !== statusUserId) throw new Error("account changed during read");
4409
+ if (subscription.tier !== sync.tier) throw new Error("tier changed during read");
4410
+ logger.info("Standalone Companion sync status", {
4411
+ tier: sync.tier,
4412
+ protocol: sync.protocol,
4413
+ syncState: sync.usage?.syncState ?? null,
4414
+ resetAtEpochMs: sync.resetAtEpochMs
4415
+ });
4416
+ } catch {
4417
+ logger.warn("Standalone Companion sync status unavailable; cloud admission remains authoritative");
4418
+ }
4419
+ if (!this.started) return { httpPort: 0 };
4420
+ await (0, import_codevibe_core6.registerDeviceEncryptionKey)(this.appSyncClient, logger);
4286
4421
  if (!this.started) return { httpPort: 0 };
4287
- (0, import_codevibe_core5.startDeviceKeyWatcher)(this.appSyncClient, logger);
4422
+ (0, import_codevibe_core6.startDeviceKeyWatcher)(this.appSyncClient, logger);
4288
4423
  try {
4289
4424
  const swept = await this.appSyncClient.sweepOrphanSessions({
4290
4425
  agentType: "ANTIGRAVITY",
@@ -4424,6 +4559,9 @@ var McpServer = class _McpServer {
4424
4559
  }
4425
4560
  this.listenerHandles = [];
4426
4561
  await Promise.allSettled([...this.transcriptHandlers]);
4562
+ await this.transcriptTailer.waitForReads();
4563
+ await this.nativeAdmissionChain;
4564
+ await this.stopNativeSources();
4427
4565
  if (this.session) {
4428
4566
  try {
4429
4567
  this.appSyncClient.stopHeartbeat(this.session.sessionId);
@@ -4434,7 +4572,7 @@ var McpServer = class _McpServer {
4434
4572
  } catch (err) {
4435
4573
  logger.warn("[tool-activity] Agy shutdown drain failed", { error: String(err) });
4436
4574
  }
4437
- const retired = await this.deactivateLaunchRow(this.session.sessionId);
4575
+ const retired = await this.deactivateLaunchRow(this.session.sessionId, this.session);
4438
4576
  if (retired) this.durableObligationOpen = false;
4439
4577
  if (!retired) {
4440
4578
  hostedRetirementError = new Error(
@@ -4516,7 +4654,7 @@ var McpServer = class _McpServer {
4516
4654
  * their settlement marker/obligation alive on `false` instead of treating the
4517
4655
  * attempt as completion. The log stays inside so no failure is ever silent.
4518
4656
  */
4519
- async deactivateLaunchRow(sessionId) {
4657
+ async deactivateLaunchRow(sessionId, quiescentSession) {
4520
4658
  try {
4521
4659
  this.appSyncClient.stopHeartbeat(sessionId);
4522
4660
  } catch (error) {
@@ -4527,7 +4665,14 @@ var McpServer = class _McpServer {
4527
4665
  }
4528
4666
  for (let attempt = 1; attempt <= SESSION_RETIRE_MAX_ATTEMPTS; attempt += 1) {
4529
4667
  try {
4530
- await this.appSyncClient.updateSession({ sessionId, status: "INACTIVE" });
4668
+ const retired = await this.appSyncClient.updateSession({ sessionId, status: "INACTIVE" });
4669
+ if (quiescentSession && retired?.sessionGenerationId) {
4670
+ await this.appSyncClient.completeDesktopSyncRetirement({ sessionId }, retired, async () => {
4671
+ await Promise.allSettled([...this.transcriptHandlers]);
4672
+ await this.nativeAdmissionChain;
4673
+ await this.stopNativeSources(quiescentSession);
4674
+ });
4675
+ }
4531
4676
  logger.info("Marked superseded/interrupted launch session INACTIVE", { sessionId });
4532
4677
  return true;
4533
4678
  } catch (err) {
@@ -4571,7 +4716,7 @@ var McpServer = class _McpServer {
4571
4716
  let sessionKey = null;
4572
4717
  let sessionKeyGen = null;
4573
4718
  try {
4574
- const result = await (0, import_codevibe_core5.resumeOrCreateSession)(
4719
+ const result = await (0, import_codevibe_core6.resumeOrCreateSession)(
4575
4720
  {
4576
4721
  sessionId,
4577
4722
  userId,
@@ -4657,8 +4802,9 @@ var McpServer = class _McpServer {
4657
4802
  const stop = this.appSyncClient.subscribeToEvents(
4658
4803
  sessionId,
4659
4804
  (evt) => {
4660
- void this.handleMobileEvent(evt).catch((err) => {
4805
+ return this.handleMobileEvent(evt).catch((err) => {
4661
4806
  logger.error("handleMobileEvent failed", { error: String(err) });
4807
+ if ((0, import_codevibe_core6.requiresSyncInputDeliveryReceipt)(evt)) throw err;
4662
4808
  });
4663
4809
  },
4664
4810
  (err) => logger.warn("AppSync subscription error", { error: String(err) }),
@@ -4740,7 +4886,7 @@ var McpServer = class _McpServer {
4740
4886
  return activeId === conversationId;
4741
4887
  }
4742
4888
  // ─── Transcript flow ────────────────────────────────────────────────────
4743
- async handleTranscriptEmit(emit) {
4889
+ async handleTranscriptEmit(emit, admission) {
4744
4890
  if (!this.started) return;
4745
4891
  const { conversationId, byteOffset, event } = emit;
4746
4892
  if (this.sessionDisabled) {
@@ -4766,14 +4912,18 @@ var McpServer = class _McpServer {
4766
4912
  }
4767
4913
  }
4768
4914
  const mapped = mapTranscriptEvent(emit, session.sessionId, notBeforeMs);
4915
+ if (admission && (mapped.events.length > 2 || mapped.events.some((input) => !["USER_PROMPT", "ASSISTANT_RESPONSE", "REASONING", "TOOL_USE", "NOTIFICATION"].includes(input.type)))) {
4916
+ throw Error("SYNC_NATIVE_SPECIALIZED_PRODUCER_REQUIRED");
4917
+ }
4769
4918
  for (const reg of mapped.pendingCallRegistrations) {
4770
4919
  this.approvalDetector.registerPendingCall(reg);
4771
4920
  }
4772
- for (const input of mapped.events) {
4773
- if (input.type === import_codevibe_core5.EventType.USER_PROMPT || input.type === import_codevibe_core5.EventType.ASSISTANT_RESPONSE || input.type === import_codevibe_core5.EventType.NOTIFICATION) {
4774
- await this.flushToolActivityAtBoundary(input.type);
4921
+ for (const [index, input] of mapped.events.entries()) {
4922
+ if (input.type === import_codevibe_core6.EventType.USER_PROMPT || input.type === import_codevibe_core6.EventType.ASSISTANT_RESPONSE || input.type === import_codevibe_core6.EventType.NOTIFICATION) {
4923
+ if (admission) await this.toolActivity.flush();
4924
+ else await this.flushToolActivityAtBoundary(input.type);
4775
4925
  }
4776
- if (input.type === import_codevibe_core5.EventType.TOOL_USE && typeof input.metadata === "object" && input.metadata) {
4926
+ if (input.type === import_codevibe_core6.EventType.TOOL_USE && typeof input.metadata === "object" && input.metadata) {
4777
4927
  const meta = input.metadata;
4778
4928
  const resultStep = typeof meta.step_index === "number" ? meta.step_index : void 0;
4779
4929
  const agyType = typeof meta.agy_type === "string" ? meta.agy_type : void 0;
@@ -4790,6 +4940,7 @@ var McpServer = class _McpServer {
4790
4940
  continue;
4791
4941
  }
4792
4942
  } catch (error) {
4943
+ if (admission) throw error;
4793
4944
  logger.warn("[tool-activity] Agy result observation failed; emitting raw TOOL_USE fallback", {
4794
4945
  sessionId: session.sessionId,
4795
4946
  conversationId,
@@ -4799,11 +4950,12 @@ var McpServer = class _McpServer {
4799
4950
  }
4800
4951
  }
4801
4952
  }
4802
- if (input.type === import_codevibe_core5.EventType.USER_PROMPT && input.source === import_codevibe_core5.EventSource.DESKTOP) {
4953
+ if (input.type === import_codevibe_core6.EventType.USER_PROMPT && input.source === import_codevibe_core6.EventSource.DESKTOP) {
4803
4954
  const clearedPromptIds = this.approvalDetector.onPanePromptCleared();
4804
4955
  await this.resolveAcceptedInputPrompts(session, clearedPromptIds);
4805
- const dedupe = this.mobileDeduper.consumeIfDuplicate(session.sessionId, input.content);
4956
+ const dedupe = admission ? null : this.mobileDeduper.consumeIfDuplicate(session.sessionId, input.content);
4806
4957
  if (dedupe) {
4958
+ if (admission) admission.echoContent = input.content;
4807
4959
  logger.info("Suppressed desktop echo of mobile prompt", {
4808
4960
  sessionId: session.sessionId,
4809
4961
  matchType: dedupe.matchType
@@ -4811,6 +4963,10 @@ var McpServer = class _McpServer {
4811
4963
  continue;
4812
4964
  }
4813
4965
  }
4966
+ if (admission) {
4967
+ admission.outputs[index] = input;
4968
+ continue;
4969
+ }
4814
4970
  try {
4815
4971
  if (!await this.createEncryptedEvent(session, input)) continue;
4816
4972
  } catch (err) {
@@ -4822,6 +4978,161 @@ var McpServer = class _McpServer {
4822
4978
  }
4823
4979
  }
4824
4980
  }
4981
+ selectNativeSource(session) {
4982
+ if (!this.started || this.session !== session) return Promise.reject(Error("SYNC_SOURCE_OWNER_CHANGED"));
4983
+ if (this.nativeSourceSelection?.session === session) return this.nativeSourceSelection.pending;
4984
+ const pending = this.appSyncClient.selectCompanionSourceTransport({ sessionId: session.sessionId });
4985
+ const selected = { session, pending };
4986
+ this.nativeSourceSelection = selected;
4987
+ void pending.catch(() => {
4988
+ if (this.nativeSourceSelection === selected) this.nativeSourceSelection = null;
4989
+ });
4990
+ return pending;
4991
+ }
4992
+ assertNativeOwner(owner) {
4993
+ if (!this.started || this.sessionDisabled || this.session !== owner.session || owner.controller.signal.aborted) {
4994
+ throw Error("SYNC_SOURCE_OWNER_CHANGED");
4995
+ }
4996
+ }
4997
+ async nativeOwner(source) {
4998
+ if (!this.started || this.sessionDisabled) throw Error("SYNC_SOURCE_STOPPED");
4999
+ await this.ensureLaunchSession();
5000
+ const session = this.session;
5001
+ if (!session || !this.isMainConversation(source.conversationId)) throw Error("SYNC_SOURCE_OWNERSHIP_UNAVAILABLE");
5002
+ const selection = await this.selectNativeSource(session);
5003
+ if (!this.started || this.session !== session) throw Error("SYNC_SOURCE_OWNER_CHANGED");
5004
+ if (selection.protocol === "LEGACY") return null;
5005
+ if (!this.toolActivity?.isActive(session.sessionId)) {
5006
+ if (this.nativeToolStart && this.nativeToolStart.session !== session) throw Error("SYNC_SOURCE_OWNER_CHANGED");
5007
+ if (!this.nativeToolStart) {
5008
+ const start = this.toolActivity ? this.toolActivity.start(session.sessionId) : Promise.reject(Error("SYNC_SOURCE_OWNERSHIP_UNAVAILABLE"));
5009
+ const attempt = { session, pending: start };
5010
+ this.nativeToolStart = attempt;
5011
+ void start.finally(() => {
5012
+ if (this.nativeToolStart === attempt) this.nativeToolStart = null;
5013
+ }).catch(() => void 0);
5014
+ }
5015
+ await this.nativeToolStart.pending;
5016
+ }
5017
+ if (!this.toolActivity?.isActive(session.sessionId) || this.session !== session || !this.started) throw Error("SYNC_SOURCE_OWNERSHIP_UNAVAILABLE");
5018
+ const streamSha256 = crypto3.createHash("sha256").update(JSON.stringify([source.filePath, source.conversationId])).digest("hex");
5019
+ const key = `${selection.sessionScope.sessionId}:${selection.sessionScope.sessionGenerationId}:${streamSha256}`;
5020
+ const existing = this.nativeSources.get(key);
5021
+ if (existing) {
5022
+ this.assertNativeOwner(existing);
5023
+ return existing;
5024
+ }
5025
+ const owner = {
5026
+ session,
5027
+ selection,
5028
+ streamSha256,
5029
+ cursor: selection.writer.sourcePair({ provider: "agy", streamSha256 }),
5030
+ controller: new AbortController(),
5031
+ drain: null,
5032
+ drainRequested: false,
5033
+ timer: null,
5034
+ retryMs: 5e3
5035
+ };
5036
+ this.nativeSources.set(key, owner);
5037
+ return owner;
5038
+ }
5039
+ async restoreNativeSource(source, initial) {
5040
+ const owner = await this.nativeOwner(source);
5041
+ if (!owner) return null;
5042
+ const position = await owner.cursor.open(initial);
5043
+ this.assertNativeOwner(owner);
5044
+ await owner.selection.writer.mobileEchoes("agy").reconcile(owner.streamSha256, position.byteOffset);
5045
+ this.drainNativeSource(owner);
5046
+ return position;
5047
+ }
5048
+ async skipNativeSource(gap, source) {
5049
+ const owner = await this.nativeOwner(source);
5050
+ if (!owner) throw Error("SYNC_SOURCE_PROTOCOL_CHANGED");
5051
+ await owner.cursor.skip(gap);
5052
+ this.assertNativeOwner(owner);
5053
+ }
5054
+ async admitNativeSource(emit, source) {
5055
+ const owner = await this.nativeOwner(source);
5056
+ if (!owner) throw Error("SYNC_SOURCE_PROTOCOL_CHANGED");
5057
+ const run = this.nativeAdmissionChain.then(async () => {
5058
+ this.assertNativeOwner(owner);
5059
+ if ((await owner.cursor.open(0)).byteOffset >= source.endOffset) return;
5060
+ const admission = { outputs: [null, null] };
5061
+ await this.handleTranscriptEmit(emit, admission);
5062
+ this.assertNativeOwner(owner);
5063
+ const echoFrame = {
5064
+ streamSha256: owner.streamSha256,
5065
+ occurrence: `${source.startOffset}:${source.endOffset}`,
5066
+ sourceSha256: source.sha256,
5067
+ startOffset: source.startOffset,
5068
+ endOffset: source.endOffset
5069
+ };
5070
+ const echoes = owner.selection.writer.mobileEchoes("agy");
5071
+ for (let index = 0; index < admission.outputs.length; index++) {
5072
+ const input = admission.outputs[index];
5073
+ if (input?.type === import_codevibe_core6.EventType.USER_PROMPT && await echoes.claim(echoFrame, input.content, emit.event.created_at)) admission.outputs[index] = null;
5074
+ }
5075
+ const factory = (index) => {
5076
+ const input = admission.outputs[index];
5077
+ return input ? (id) => owner.selection.prepareEvent(input, id) : null;
5078
+ };
5079
+ await owner.cursor.admit({
5080
+ provider: "agy",
5081
+ streamSha256: owner.streamSha256,
5082
+ occurrence: `${source.startOffset}:${source.endOffset}`,
5083
+ sourceSha256: source.sha256,
5084
+ startOffset: source.startOffset,
5085
+ endOffset: source.endOffset,
5086
+ anchor: source.anchor
5087
+ }, [factory(0), factory(1)]);
5088
+ this.assertNativeOwner(owner);
5089
+ await echoes.reconcile(owner.streamSha256, source.endOffset);
5090
+ this.drainNativeSource(owner);
5091
+ });
5092
+ this.nativeAdmissionChain = run.catch((error) => {
5093
+ logger.warn("[sync-source] Agy admission retained", { error: String(error) });
5094
+ });
5095
+ await run;
5096
+ }
5097
+ drainNativeSource(owner) {
5098
+ if (!this.started || this.session !== owner.session || owner.controller.signal.aborted) return;
5099
+ owner.drainRequested = true;
5100
+ if (owner.drain || owner.timer) return;
5101
+ owner.drainRequested = false;
5102
+ const run = (async () => {
5103
+ let failed = false;
5104
+ try {
5105
+ if (await owner.cursor.drain(64, owner.controller.signal) >= 64) owner.drainRequested = true;
5106
+ owner.retryMs = 5e3;
5107
+ } catch (error) {
5108
+ failed = true;
5109
+ owner.retryMs = Math.min(owner.retryMs * 2, 6e4);
5110
+ if (!owner.controller.signal.aborted) logger.warn("[sync-source] Agy delivery retained", { error: String(error) });
5111
+ }
5112
+ owner.drain = null;
5113
+ if (!this.started || this.session !== owner.session || owner.controller.signal.aborted) return;
5114
+ if (failed) {
5115
+ owner.timer = setTimeout(() => {
5116
+ owner.timer = null;
5117
+ this.drainNativeSource(owner);
5118
+ }, owner.retryMs);
5119
+ owner.timer.unref();
5120
+ } else if (owner.drainRequested) this.drainNativeSource(owner);
5121
+ })();
5122
+ owner.drain = run;
5123
+ void run.catch((error) => logger.error("[sync-source] unexpected Agy drain failure", { error: String(error) }));
5124
+ }
5125
+ async stopNativeSources(session) {
5126
+ const owned = [...this.nativeSources].filter(([, owner]) => !session || owner.session === session);
5127
+ for (const [, owner] of owned) {
5128
+ if (owner.timer) clearTimeout(owner.timer);
5129
+ owner.timer = null;
5130
+ owner.controller.abort(Error("SYNC_SOURCE_STOPPED"));
5131
+ }
5132
+ await Promise.all(owned.map(([, owner]) => owner.drain?.catch(() => void 0)));
5133
+ for (const [key] of owned) this.nativeSources.delete(key);
5134
+ if (!session || this.nativeSourceSelection?.session === session) this.nativeSourceSelection = null;
5135
+ }
4825
5136
  async flushToolActivityAtBoundary(boundary) {
4826
5137
  try {
4827
5138
  await this.toolActivity?.flush();
@@ -4851,18 +5162,18 @@ var McpServer = class _McpServer {
4851
5162
  const expectedSession = this.session;
4852
5163
  try {
4853
5164
  const session = await this.appSyncClient.getSession(sessionId);
4854
- if (session?.status === import_codevibe_core5.SessionStatus.INACTIVE) {
5165
+ if (session?.status === import_codevibe_core6.SessionStatus.INACTIVE) {
4855
5166
  const terminal = new Error(`Antigravity session is no longer active: ${sessionId}`);
4856
5167
  terminal.errorType = "SessionTerminal";
4857
5168
  throw terminal;
4858
5169
  }
4859
- if (!session || session.status !== import_codevibe_core5.SessionStatus.ACTIVE || !session.isEncrypted) {
5170
+ if (!session || session.status !== import_codevibe_core6.SessionStatus.ACTIVE || !session.isEncrypted) {
4860
5171
  return null;
4861
5172
  }
4862
5173
  const sessionKeyGen = session.sessionKeyGen ?? null;
4863
5174
  const encryptedKeys = session.encryptedKeys ?? [];
4864
5175
  if (encryptedKeys.length === 0) return null;
4865
- const sessionKey = await import_codevibe_core5.keychainManager.getSessionKey(
5176
+ const sessionKey = await import_codevibe_core6.keychainManager.getSessionKey(
4866
5177
  sessionId,
4867
5178
  encryptedKeys,
4868
5179
  sessionKeyGen
@@ -4875,7 +5186,7 @@ var McpServer = class _McpServer {
4875
5186
  liveSession.sessionIsEncrypted = true;
4876
5187
  return { sessionKey, sessionKeyGen };
4877
5188
  } catch (error) {
4878
- if ((0, import_codevibe_core5.isTerminalSessionError)(error)) throw error;
5189
+ if ((0, import_codevibe_core6.isTerminalSessionError)(error)) throw error;
4879
5190
  logger.warn("[tool-activity] Agy session-key refresh failed; window remains retryable", {
4880
5191
  sessionId,
4881
5192
  error: String(error)
@@ -4895,7 +5206,7 @@ var McpServer = class _McpServer {
4895
5206
  * an encrypted keyless one (drop).
4896
5207
  */
4897
5208
  encryptOutbound(session, input) {
4898
- const enc = (0, import_codevibe_core5.encryptForEmit)(
5209
+ const enc = (0, import_codevibe_core6.encryptForEmit)(
4899
5210
  session.sessionKey ?? null,
4900
5211
  session.sessionIsEncrypted ?? false,
4901
5212
  input.content,
@@ -4925,7 +5236,7 @@ var McpServer = class _McpServer {
4925
5236
  const promptId = input.producerKind === "PLUGIN_APPROVAL" ? input.promptId : void 0;
4926
5237
  const promptEntry = promptId ? this.e1PromptRaises.get(promptId) : void 0;
4927
5238
  const sendOnce = async () => {
4928
- if (promptId && !(0, import_codevibe_core5.isOpenPromptRaise)(
5239
+ if (promptId && !(0, import_codevibe_core6.isOpenPromptRaise)(
4929
5240
  promptEntry,
4930
5241
  this.e1PromptRaises.get(promptId),
4931
5242
  this.e1NeedsResolution.has(promptId)
@@ -4938,7 +5249,7 @@ var McpServer = class _McpServer {
4938
5249
  try {
4939
5250
  return await sendOnce();
4940
5251
  } catch (error) {
4941
- if (!(0, import_codevibe_core5.isSessionKeyStaleError)(error)) throw error;
5252
+ if (!(0, import_codevibe_core6.isSessionKeyStaleError)(error)) throw error;
4942
5253
  const refreshed = await this.refreshToolActivitySessionKey(session.sessionId);
4943
5254
  if (!refreshed) throw error;
4944
5255
  return sendOnce();
@@ -4959,7 +5270,7 @@ var McpServer = class _McpServer {
4959
5270
  * NOTIFICATION carrier, no fabricated options).
4960
5271
  */
4961
5272
  newE1Raise(sessionId, mobileActionable, title) {
4962
- const { record } = (0, import_codevibe_core5.newPromptRaise)({
5273
+ const { record } = (0, import_codevibe_core6.newPromptRaise)({
4963
5274
  sessionId,
4964
5275
  producerKind: "PLUGIN_APPROVAL",
4965
5276
  mobileActionable,
@@ -4995,14 +5306,12 @@ var McpServer = class _McpServer {
4995
5306
  const operation = (async () => {
4996
5307
  try {
4997
5308
  if (this.promptPublications.isPublishing(promptId)) await this.promptPublications.waitFor(promptId);
4998
- const ack = await this.appSyncClient.createEvent({
5309
+ const acknowledged = await this.appSyncClient.resolveCompanionPrompt({
4999
5310
  sessionId: entry.record.sessionId || sessionId,
5000
- type: import_codevibe_core5.EventType.NOTIFICATION,
5001
- source: import_codevibe_core5.EventSource.DESKTOP,
5002
- resolvedPromptId: promptId,
5311
+ promptId,
5003
5312
  ownerToken: entry.record.ownerToken
5004
5313
  });
5005
- if (ack?.promptId !== promptId) return false;
5314
+ if (!acknowledged) return false;
5006
5315
  this.e1NeedsResolution.delete(promptId);
5007
5316
  this.e1NeedsReRaise.delete(promptId);
5008
5317
  this.e1PromptRaises.delete(promptId);
@@ -5055,27 +5364,43 @@ var McpServer = class _McpServer {
5055
5364
  * Content is E2E-encrypted; the raise identity rides plaintext top-level.
5056
5365
  * Fail-closed on a missing session key — never cleartext.
5057
5366
  */
5058
- async emitSuppressedPromptCarrier(session, record, customBannerText) {
5367
+ async emitSuppressedPromptCarrier(session, record, reRaiseToken, customBannerText) {
5059
5368
  const bannerText = customBannerText || "\u26A0\uFE0F A prompt is waiting in your desktop terminal, but its options could not be shown here. Please answer it on your desktop.";
5060
5369
  const input = {
5061
5370
  sessionId: session.sessionId,
5062
- type: import_codevibe_core5.EventType.NOTIFICATION,
5063
- source: import_codevibe_core5.EventSource.DESKTOP,
5371
+ type: import_codevibe_core6.EventType.NOTIFICATION,
5372
+ source: import_codevibe_core6.EventSource.DESKTOP,
5064
5373
  content: bannerText,
5065
5374
  metadata: {
5066
5375
  e1SuppressedPrompt: true
5067
5376
  },
5068
- ...(0, import_codevibe_core5.raiseFieldsFromRecord)(record),
5377
+ ...(0, import_codevibe_core6.raiseFieldsFromRecord)(record),
5069
5378
  notificationText: bannerText,
5070
- timestamp: (0, import_codevibe_core5.prepareEventTimestamp)({ orderingKey: session.sessionId })
5379
+ timestamp: (0, import_codevibe_core6.prepareEventTimestamp)({ orderingKey: session.sessionId })
5071
5380
  };
5072
- await this.flushToolActivityAtBoundary(import_codevibe_core5.EventType.NOTIFICATION);
5073
- if (!await this.createEncryptedEvent(session, input)) return false;
5074
- logger.info("E1: emitted suppressed-prompt badge carrier (mobileActionable=false)", {
5075
- sessionId: session.sessionId,
5076
- promptId: record.promptId
5077
- });
5078
- return true;
5381
+ await this.flushToolActivityAtBoundary(import_codevibe_core6.EventType.NOTIFICATION);
5382
+ return this.publishE1Prompt(session, input, reRaiseToken);
5383
+ }
5384
+ async publishE1Prompt(session, input, reRaiseToken) {
5385
+ const promptId = input.promptId, entry = promptId ? this.e1PromptRaises.get(promptId) : void 0;
5386
+ if (!promptId || !(0, import_codevibe_core6.isOpenPromptRaise)(entry, this.e1PromptRaises.get(promptId), this.e1NeedsResolution.has(promptId))) return "CLOSED";
5387
+ if (!this.started || this.session !== session || input.sessionId !== session.sessionId) return "RETRY";
5388
+ const { notificationText: _legacyHint, ...plain } = input;
5389
+ const settle = (disposition) => {
5390
+ if ((0, import_codevibe_core6.isOpenPromptRaise)(entry, this.e1PromptRaises.get(promptId), this.e1NeedsResolution.has(promptId))) {
5391
+ if (disposition === "RETRY") this.e1NeedsReRaise.add(promptId);
5392
+ else if (disposition === "RERAISED" || disposition === "OMITTED") this.e1NeedsReRaise.delete(promptId);
5393
+ }
5394
+ return disposition;
5395
+ };
5396
+ return this.promptPublications.run(promptId, () => this.appSyncClient.publishCompanionPrompt(
5397
+ plain,
5398
+ async () => !!await this.createEncryptedEvent(session, input),
5399
+ { reRaiseToken }
5400
+ ).then(settle, (error) => {
5401
+ settle("RETRY");
5402
+ throw error;
5403
+ }));
5079
5404
  }
5080
5405
  /**
5081
5406
  * E1 (§6a-2b) — badge-only raise for a SUPPRESSED prompt, applying the content-key
@@ -5100,7 +5425,7 @@ var McpServer = class _McpServer {
5100
5425
  entry.contentPlain = customBannerText;
5101
5426
  }
5102
5427
  }
5103
- this.emitSuppressedPromptCarrier(session, record, customBannerText).catch((e) => {
5428
+ this.emitSuppressedPromptCarrier(session, record, void 0, customBannerText).catch((e) => {
5104
5429
  logger.error("E1: failed to emit suppressed-prompt badge carrier", {
5105
5430
  sessionId: session.sessionId,
5106
5431
  promptId: record.promptId,
@@ -5137,8 +5462,8 @@ var McpServer = class _McpServer {
5137
5462
  ...snap.metadataPlain !== void 0 && { metadataPlain: snap.metadataPlain }
5138
5463
  };
5139
5464
  this.e1PromptRaises.set(entry.record.promptId, entry);
5140
- const acked = await this.reRaiseOneE1(replacementSession, entry, opts);
5141
- if (!acked && !entry.resolutionStarted) unacked.push(snap);
5465
+ const disposition = await this.reRaiseOneE1(replacementSession, entry, opts);
5466
+ if (disposition !== "RERAISED" && disposition !== "OMITTED" && !entry.resolutionStarted) unacked.push(snap);
5142
5467
  }
5143
5468
  if (!this.e1TtlRefreshTimer) this.startE1TtlRefresh();
5144
5469
  return unacked;
@@ -5226,57 +5551,65 @@ var McpServer = class _McpServer {
5226
5551
  */
5227
5552
  async reRaiseOneE1(session, entry, opts) {
5228
5553
  const rec = entry.record;
5229
- if (this.e1NeedsResolution.has(rec.promptId)) return false;
5230
- if (!(0, import_codevibe_core5.canRecoverPromptRaise)(entry, this.promptPublications.isPublishing(rec.promptId))) {
5231
- return false;
5554
+ if (this.e1NeedsResolution.has(rec.promptId)) return "CLOSED";
5555
+ if (!(0, import_codevibe_core6.canRecoverPromptRaise)(entry, this.promptPublications.isPublishing(rec.promptId))) {
5556
+ return "RETRY";
5232
5557
  }
5233
5558
  const actionable = rec.mobileActionable && entry.contentPlain !== void 0;
5234
5559
  const retryUntilAck = opts?.retryUntilAck === true;
5235
5560
  const gen = opts?.gen;
5236
5561
  const MAX_ATTEMPTS = 4;
5237
5562
  for (let attempt = 1; ; attempt++) {
5238
- if (!(0, import_codevibe_core5.isOpenPromptRaise)(
5563
+ if (!(0, import_codevibe_core6.isOpenPromptRaise)(
5239
5564
  entry,
5240
5565
  this.e1PromptRaises.get(rec.promptId),
5241
5566
  this.e1NeedsResolution.has(rec.promptId)
5242
- )) return false;
5567
+ )) return "CLOSED";
5243
5568
  if (retryUntilAck && this.reRaiseSuperseded(gen)) {
5244
5569
  this.e1NeedsReRaise.add(rec.promptId);
5245
5570
  logger.warn("E1: block-until-ack re-raise aborted (lifecycle superseded/stopping) \u2014 flagged for retry-until-ack (entry retained)", {
5246
5571
  sessionId: session.sessionId,
5247
5572
  promptId: rec.promptId
5248
5573
  });
5249
- return false;
5574
+ return "RETRY";
5250
5575
  }
5251
5576
  try {
5577
+ let disposition;
5252
5578
  if (actionable) {
5253
5579
  const input = {
5254
5580
  sessionId: session.sessionId,
5255
- type: import_codevibe_core5.EventType.INTERACTIVE_PROMPT,
5256
- source: import_codevibe_core5.EventSource.DESKTOP,
5581
+ type: import_codevibe_core6.EventType.INTERACTIVE_PROMPT,
5582
+ source: import_codevibe_core6.EventSource.DESKTOP,
5257
5583
  content: entry.contentPlain,
5258
5584
  metadata: entry.metadataPlain ?? {},
5259
- ...(0, import_codevibe_core5.raiseFieldsFromRecord)(rec),
5260
- timestamp: (0, import_codevibe_core5.prepareEventTimestamp)({ orderingKey: session.sessionId })
5585
+ ...(0, import_codevibe_core6.raiseFieldsFromRecord)(rec),
5586
+ timestamp: (0, import_codevibe_core6.prepareEventTimestamp)({ orderingKey: session.sessionId })
5261
5587
  };
5262
- await this.flushToolActivityAtBoundary(import_codevibe_core5.EventType.INTERACTIVE_PROMPT);
5263
- if (!(0, import_codevibe_core5.isOpenPromptRaise)(
5588
+ await this.flushToolActivityAtBoundary(import_codevibe_core6.EventType.INTERACTIVE_PROMPT);
5589
+ if (!(0, import_codevibe_core6.isOpenPromptRaise)(
5264
5590
  entry,
5265
5591
  this.e1PromptRaises.get(rec.promptId),
5266
5592
  this.e1NeedsResolution.has(rec.promptId)
5267
- )) return false;
5268
- if (!await this.createEncryptedEvent(session, input)) return false;
5593
+ )) return "CLOSED";
5594
+ disposition = await this.publishE1Prompt(session, input, opts?.reRaiseToken);
5269
5595
  } else {
5270
5596
  rec.mobileActionable = false;
5271
- if (!await this.emitSuppressedPromptCarrier(session, rec, entry.contentPlain)) return false;
5597
+ disposition = await this.emitSuppressedPromptCarrier(
5598
+ session,
5599
+ rec,
5600
+ opts?.reRaiseToken,
5601
+ entry.contentPlain
5602
+ );
5272
5603
  }
5604
+ if (disposition === "CLOSED" || disposition === "RETRY") return disposition;
5273
5605
  this.e1NeedsReRaise.delete(rec.promptId);
5274
- logger.info("E1: re-raised prompt onto replacement session", {
5606
+ logger.info("E1: prompt publication settled", {
5275
5607
  sessionId: session.sessionId,
5276
5608
  promptId: rec.promptId,
5277
- actionable
5609
+ actionable,
5610
+ disposition
5278
5611
  });
5279
- return true;
5612
+ return disposition;
5280
5613
  } catch (e) {
5281
5614
  if (!retryUntilAck && attempt >= MAX_ATTEMPTS) {
5282
5615
  this.e1NeedsReRaise.add(rec.promptId);
@@ -5285,7 +5618,7 @@ var McpServer = class _McpServer {
5285
5618
  promptId: rec.promptId,
5286
5619
  error: e instanceof Error ? e.message : String(e)
5287
5620
  });
5288
- return false;
5621
+ return "RETRY";
5289
5622
  }
5290
5623
  if (retryUntilAck) {
5291
5624
  const delay2 = Math.min(
@@ -5333,16 +5666,14 @@ var McpServer = class _McpServer {
5333
5666
  const session = this.session;
5334
5667
  if (!session) return;
5335
5668
  const sid = session.sessionId;
5336
- const promptIds = [...this.e1PromptRaises.values()].filter((e) => e.record.sessionId === sid && !this.e1NeedsResolution.has(e.record.promptId) && (0, import_codevibe_core5.canRecoverPromptRaise)(e, this.promptPublications.isPublishing(e.record.promptId))).map((e) => e.record.promptId);
5669
+ const promptIds = [...this.e1PromptRaises.values()].filter((e) => e.record.sessionId === sid && !this.e1NeedsResolution.has(e.record.promptId) && (0, import_codevibe_core6.canRecoverPromptRaise)(e, this.promptPublications.isPublishing(e.record.promptId))).map((e) => e.record.promptId);
5337
5670
  if (promptIds.length === 0) return;
5338
5671
  try {
5339
- const agedOut = await this.appSyncClient.refreshOpenPromptTtl(promptIds);
5340
- for (const promptId of agedOut) {
5672
+ await this.appSyncClient.refreshSessionOpenPromptTtl({ sessionId: sid, promptIds }, async (promptId, reRaiseToken) => {
5341
5673
  const entry = this.e1PromptRaises.get(promptId);
5342
- if (entry && this.session && entry.record.sessionId === this.session.sessionId) {
5343
- await this.reRaiseOneE1(this.session, entry);
5344
- }
5345
- }
5674
+ if (!entry || this.session !== session || entry.record.sessionId !== sid || this.e1NeedsResolution.has(promptId)) return "CLOSED";
5675
+ return this.reRaiseOneE1(session, entry, { reRaiseToken });
5676
+ });
5346
5677
  } catch (e) {
5347
5678
  logger.warn("E1: TTL-refresh tick failed (non-fatal, retries next tick)", {
5348
5679
  error: e instanceof Error ? e.message : String(e)
@@ -5403,6 +5734,7 @@ var McpServer = class _McpServer {
5403
5734
  }
5404
5735
  this.subscription = null;
5405
5736
  }
5737
+ await this.stopNativeSources(old);
5406
5738
  try {
5407
5739
  await this.toolActivity?.prepareForReplacement();
5408
5740
  } catch (error) {
@@ -5445,10 +5777,10 @@ var McpServer = class _McpServer {
5445
5777
  }
5446
5778
  const result = { ...evt };
5447
5779
  try {
5448
- result.content = import_codevibe_core5.cryptoService.decryptContent(evt.content, session.sessionKey);
5780
+ result.content = import_codevibe_core6.cryptoService.decryptContent(evt.content, session.sessionKey);
5449
5781
  const rawMeta = parseMaybeJson(evt.metadata);
5450
5782
  if (rawMeta && typeof rawMeta === "object" && typeof rawMeta.encrypted === "string") {
5451
- const decryptedMeta = import_codevibe_core5.cryptoService.decryptMetadata(
5783
+ const decryptedMeta = import_codevibe_core6.cryptoService.decryptMetadata(
5452
5784
  rawMeta.encrypted,
5453
5785
  session.sessionKey
5454
5786
  );
@@ -5572,35 +5904,35 @@ var McpServer = class _McpServer {
5572
5904
  const content = state.paneDisplayHeader || state.matchedPaneHeader || state.pendingCall?.command || state.pendingCall?.filePath || "approval requested";
5573
5905
  const input = {
5574
5906
  sessionId: session.sessionId,
5575
- type: import_codevibe_core5.EventType.INTERACTIVE_PROMPT,
5576
- source: import_codevibe_core5.EventSource.DESKTOP,
5907
+ type: import_codevibe_core6.EventType.INTERACTIVE_PROMPT,
5908
+ source: import_codevibe_core6.EventSource.DESKTOP,
5577
5909
  content,
5578
5910
  metadata: optionsForMobile,
5579
5911
  // E1 (§4) — raise identity top-level: promptId + ownerToken/producerKind/
5580
5912
  // mobileActionable (the backend's classifyRaise upserts the open-prompt row).
5581
- ...(0, import_codevibe_core5.raiseFieldsFromRecord)(e1rec),
5913
+ ...(0, import_codevibe_core6.raiseFieldsFromRecord)(e1rec),
5582
5914
  // Per-conversation monotonic timestamp (event-timestamp-ordering fix
5583
5915
  // v5.4, 2026-05-24). orderingKey scopes the counter to this conv so
5584
5916
  // INTERACTIVE_PROMPT can't be forced to lastMs+1 by a newer event
5585
5917
  // from a sibling conversation under v9's multi-conv-per-session
5586
5918
  // model.
5587
- timestamp: (0, import_codevibe_core5.prepareEventTimestamp)({
5919
+ timestamp: (0, import_codevibe_core6.prepareEventTimestamp)({
5588
5920
  orderingKey: `${session.sessionId}:${state.conversationId}`
5589
5921
  })
5590
5922
  };
5591
5923
  try {
5592
5924
  this.stashE1RaiseContent(e1rec.promptId, content, optionsForMobile);
5593
- await this.flushToolActivityAtBoundary(import_codevibe_core5.EventType.INTERACTIVE_PROMPT);
5925
+ await this.flushToolActivityAtBoundary(import_codevibe_core6.EventType.INTERACTIVE_PROMPT);
5594
5926
  if (this.e1NeedsResolution.has(e1rec.promptId) || this.e1PromptRaises.get(e1rec.promptId)?.record !== e1rec) return;
5595
- if (!await this.createEncryptedEvent(session, input)) return;
5927
+ await this.publishE1Prompt(session, input);
5596
5928
  } catch (err) {
5597
5929
  logger.error("createEvent INTERACTIVE_PROMPT failed", {
5598
5930
  promptId: state.promptId,
5599
5931
  error: String(err)
5600
5932
  });
5601
- this.approvalDetector.rollbackPrompt(state.promptId);
5602
- if (state.paneHash) this.paneObserver.resetPromptHash(state.paneHash);
5603
- this.e1PromptRaises.delete(state.promptId);
5933
+ if (this.e1PromptRaises.get(e1rec.promptId)?.record === e1rec && !this.e1NeedsResolution.has(e1rec.promptId)) {
5934
+ this.e1NeedsReRaise.add(e1rec.promptId);
5935
+ }
5604
5936
  }
5605
5937
  }
5606
5938
  // ─── Mobile → desktop flow ──────────────────────────────────────────────
@@ -5638,7 +5970,7 @@ var McpServer = class _McpServer {
5638
5970
  if (shouldDecrypt && sessionKey) {
5639
5971
  try {
5640
5972
  logger.info("Decrypting attachment", { id: attachment.id });
5641
- buffer = import_codevibe_core5.cryptoService.decryptData(buffer, sessionKey);
5973
+ buffer = import_codevibe_core6.cryptoService.decryptData(buffer, sessionKey);
5642
5974
  logger.info("Attachment decrypted successfully", {
5643
5975
  id: attachment.id,
5644
5976
  decryptedSize: buffer.length
@@ -5693,7 +6025,7 @@ var McpServer = class _McpServer {
5693
6025
  await this.ensureLaunchSession();
5694
6026
  if (!this.session) return;
5695
6027
  const session = this.session;
5696
- if (evt.source !== import_codevibe_core5.EventSource.MOBILE) return;
6028
+ if (evt.source !== import_codevibe_core6.EventSource.MOBILE) return;
5697
6029
  if (evt.sessionId !== session.sessionId) {
5698
6030
  logger.warn("Dropping mobile event with mismatched sessionId", {
5699
6031
  expected: session.sessionId,
@@ -5704,19 +6036,20 @@ var McpServer = class _McpServer {
5704
6036
  let decrypted;
5705
6037
  try {
5706
6038
  decrypted = this.decryptInbound(session, evt);
5707
- } catch {
6039
+ } catch (error) {
6040
+ if ((0, import_codevibe_core6.requiresSyncInputDeliveryReceipt)(evt)) throw error;
5708
6041
  return;
5709
6042
  }
5710
6043
  evt = decrypted;
5711
- await this.markDelivered(evt);
5712
- if (evt.type === import_codevibe_core5.EventType.USER_PROMPT || evt.type === import_codevibe_core5.EventType.PROMPT_RESPONSE) {
6044
+ if (!await this.markDelivered(evt)) return;
6045
+ if (evt.type === import_codevibe_core6.EventType.USER_PROMPT || evt.type === import_codevibe_core6.EventType.PROMPT_RESPONSE) {
5713
6046
  const mobileFloorMs = Date.parse(evt.timestamp);
5714
6047
  this.mobilePromptFloorBySession.set(session.sessionId, {
5715
6048
  floorMs: Number.isNaN(mobileFloorMs) ? Date.now() : mobileFloorMs,
5716
6049
  setAtWallMs: Date.now()
5717
6050
  });
5718
6051
  }
5719
- if (evt.type === import_codevibe_core5.EventType.USER_PROMPT || evt.type === import_codevibe_core5.EventType.PROMPT_RESPONSE) {
6052
+ if (evt.type === import_codevibe_core6.EventType.USER_PROMPT || evt.type === import_codevibe_core6.EventType.PROMPT_RESPONSE) {
5720
6053
  let promptContent = evt.content;
5721
6054
  const attachments = evt.attachments ?? [];
5722
6055
  if (attachments.length > 0) {
@@ -5755,16 +6088,16 @@ var McpServer = class _McpServer {
5755
6088
  const rawMeta = parseMaybeJson(evt.metadata);
5756
6089
  const meta = rawMeta && typeof rawMeta === "object" ? rawMeta : {};
5757
6090
  const evtAny = evt;
5758
- 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;
6091
+ const responsePromptId = evt.type === import_codevibe_core6.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;
5759
6092
  const advertisedOptionNumbers = Array.from(new Set(
5760
6093
  pendingPrompts.flatMap((prompt) => Object.keys(prompt.submitMap))
5761
6094
  ));
5762
- const intent = (0, import_codevibe_core5.classifyMobilePromptInput)(
6095
+ const intent = (0, import_codevibe_core6.classifyMobilePromptInput)(
5763
6096
  promptContent,
5764
6097
  advertisedOptionNumbers
5765
6098
  );
5766
6099
  if (intent.kind === "option") {
5767
- const activePrompt = evt.type === import_codevibe_core5.EventType.PROMPT_RESPONSE ? pendingPrompts.find((prompt) => prompt.promptId === responsePromptId) ?? null : pendingPrompts.length === 1 ? pendingPrompts[0] : null;
6100
+ const activePrompt = evt.type === import_codevibe_core6.EventType.PROMPT_RESPONSE ? pendingPrompts.find((prompt) => prompt.promptId === responsePromptId) ?? null : pendingPrompts.length === 1 ? pendingPrompts[0] : null;
5768
6101
  if (!activePrompt || !Object.prototype.hasOwnProperty.call(activePrompt.submitMap, intent.option)) {
5769
6102
  logger.warn("Numeric input does not identify one current advertised prompt; dropping", {
5770
6103
  sessionId: session.sessionId,
@@ -5774,7 +6107,7 @@ var McpServer = class _McpServer {
5774
6107
  });
5775
6108
  return;
5776
6109
  }
5777
- if (evt.type === import_codevibe_core5.EventType.PROMPT_RESPONSE) {
6110
+ if (evt.type === import_codevibe_core6.EventType.PROMPT_RESPONSE) {
5778
6111
  if (responsePromptId !== activePrompt.promptId) {
5779
6112
  logger.warn("PROMPT_RESPONSE does not identify the current advertised prompt; dropping", {
5780
6113
  sessionId: session.sessionId,
@@ -5784,13 +6117,13 @@ var McpServer = class _McpServer {
5784
6117
  return;
5785
6118
  }
5786
6119
  }
5787
- this.mobileDeduper.track(session.sessionId, intent.option);
6120
+ await this.trackDurableMobileEcho(session, evt, intent.option, "primary");
5788
6121
  const optionResult = await this.promptResponder.sendApprovalReply(
5789
6122
  activePrompt.promptId,
5790
6123
  intent.option
5791
6124
  );
5792
6125
  if (!optionResult.ok) {
5793
- this.mobileDeduper.forget(session.sessionId, intent.option);
6126
+ if (!(0, import_codevibe_core6.requiresSyncInputDeliveryReceipt)(evt)) this.mobileDeduper.forget(session.sessionId, intent.option);
5794
6127
  return;
5795
6128
  }
5796
6129
  await this.resolveE1PluginApproval(session.sessionId, activePrompt.promptId);
@@ -5812,10 +6145,12 @@ var McpServer = class _McpServer {
5812
6145
  const clearedPromptIds = this.approvalDetector.onPanePromptCleared();
5813
6146
  await this.resolveAcceptedInputPrompts(session, clearedPromptIds);
5814
6147
  }
5815
- this.mobileDeduper.track(session.sessionId, promptContent);
5816
- const result = await this.promptResponder.sendFreeFormWhenNoApprovalActive(promptContent);
6148
+ const result = await this.promptResponder.sendFreeFormWhenNoApprovalActive(
6149
+ promptContent,
6150
+ () => this.trackDurableMobileEcho(session, evt, promptContent, "free_form")
6151
+ );
5817
6152
  if (!result.ok) {
5818
- this.mobileDeduper.forget(session.sessionId, promptContent);
6153
+ if (!(0, import_codevibe_core6.requiresSyncInputDeliveryReceipt)(evt)) this.mobileDeduper.forget(session.sessionId, promptContent);
5819
6154
  logger.warn("sendFreeForm failed", { reason: result.reason, details: result.details });
5820
6155
  return;
5821
6156
  }
@@ -5823,18 +6158,37 @@ var McpServer = class _McpServer {
5823
6158
  return;
5824
6159
  }
5825
6160
  }
6161
+ async trackDurableMobileEcho(session, event, content, lane) {
6162
+ if (!(0, import_codevibe_core6.requiresSyncInputDeliveryReceipt)(event)) {
6163
+ this.mobileDeduper.track(session.sessionId, content);
6164
+ return;
6165
+ }
6166
+ const selected = await this.selectNativeSource(session);
6167
+ if (selected.protocol !== "SYNC_V2" || !this.started || this.sessionDisabled || this.session !== session || event.sessionId !== selected.sessionScope.sessionId || event.sessionGenerationId !== selected.sessionScope.sessionGenerationId) throw Error("SYNC_ECHO_OWNER_CHANGED");
6168
+ if (content.trim()) await selected.writer.mobileEchoes("agy").arm(
6169
+ { eventId: event.eventId, timestamp: event.timestamp, type: event.type, lane },
6170
+ content,
6171
+ { nowMs: Date.now(), windowMs: 15e3, clockPrecisionMs: 1e3, allowSuffix: true, normalization: "whitespace" }
6172
+ );
6173
+ if (!this.started || this.sessionDisabled || this.session !== session) throw Error("SYNC_ECHO_OWNER_CHANGED");
6174
+ }
5826
6175
  async emitPromptSafetyNotification(session, content) {
5827
6176
  const input = {
5828
6177
  sessionId: session.sessionId,
5829
- type: import_codevibe_core5.EventType.NOTIFICATION,
5830
- source: import_codevibe_core5.EventSource.DESKTOP,
6178
+ type: import_codevibe_core6.EventType.NOTIFICATION,
6179
+ source: import_codevibe_core6.EventSource.DESKTOP,
5831
6180
  content,
5832
6181
  metadata: { promptSafetyBlocked: true },
5833
- timestamp: (0, import_codevibe_core5.prepareEventTimestamp)({ orderingKey: session.sessionId })
6182
+ timestamp: (0, import_codevibe_core6.prepareEventTimestamp)({ orderingKey: session.sessionId })
5834
6183
  };
5835
- await this.flushToolActivityAtBoundary(import_codevibe_core5.EventType.NOTIFICATION);
6184
+ await this.flushToolActivityAtBoundary(import_codevibe_core6.EventType.NOTIFICATION);
5836
6185
  try {
5837
- await this.createEncryptedEvent(session, input);
6186
+ if (!this.started || this.sessionDisabled || this.session !== session) return;
6187
+ const result = await this.appSyncClient.publishCompanionNotice(
6188
+ input,
6189
+ async () => !!await this.createEncryptedEvent(session, input)
6190
+ );
6191
+ logger.debug("Prompt-safety notification publication outcome", { sessionId: session.sessionId, result });
5838
6192
  } catch (error) {
5839
6193
  logger.warn("Failed to emit prompt-safety notification", { error: String(error) });
5840
6194
  }
@@ -5844,23 +6198,20 @@ var McpServer = class _McpServer {
5844
6198
  * soon as the plugin receives + decrypts the event and BEFORE the
5845
6199
  * tmux send is attempted, mirroring codex/claude plugin semantics:
5846
6200
  * "delivered" means the desktop daemon has the message in hand, not
5847
- * that it has acted on it yet. Errors are swallowed (logged) since
5848
- * a status-write failure must not block the actual prompt routing.
6201
+ * that it has acted on it yet. Legacy checkmarks remain best-effort;
6202
+ * R2 requires exact cloud authority before terminal routing.
5849
6203
  */
5850
6204
  async markDelivered(evt) {
5851
6205
  try {
5852
- await this.appSyncClient.updateEventStatus({
5853
- eventId: evt.eventId,
5854
- sessionId: evt.sessionId,
5855
- timestamp: evt.timestamp,
5856
- deliveryStatus: import_codevibe_core5.DeliveryStatus.DELIVERED
5857
- });
6206
+ return await (0, import_codevibe_core6.acknowledgeMobileInputDelivery)(this.appSyncClient.updateEventStatus.bind(this.appSyncClient), evt);
5858
6207
  } catch (err) {
5859
6208
  logger.warn("updateEventStatus(DELIVERED) failed", {
5860
6209
  eventId: evt.eventId,
5861
6210
  sessionId: evt.sessionId,
5862
6211
  error: String(err)
5863
6212
  });
6213
+ if ((0, import_codevibe_core6.requiresSyncInputDeliveryReceipt)(evt)) throw err;
6214
+ return true;
5864
6215
  }
5865
6216
  }
5866
6217
  /**
@@ -5873,12 +6224,7 @@ var McpServer = class _McpServer {
5873
6224
  */
5874
6225
  async markExecuted(evt) {
5875
6226
  try {
5876
- await this.appSyncClient.updateEventStatus({
5877
- eventId: evt.eventId,
5878
- sessionId: evt.sessionId,
5879
- timestamp: evt.timestamp,
5880
- deliveryStatus: import_codevibe_core5.DeliveryStatus.EXECUTED
5881
- });
6227
+ await (0, import_codevibe_core6.acknowledgeMobileInputExecution)(this.appSyncClient.updateEventStatus.bind(this.appSyncClient), evt);
5882
6228
  } catch (err) {
5883
6229
  logger.warn("updateEventStatus(EXECUTED) failed", {
5884
6230
  eventId: evt.eventId,
@@ -5889,10 +6235,14 @@ var McpServer = class _McpServer {
5889
6235
  }
5890
6236
  // ─── HTTP API push hook (internal/programmatic) ────────────────────────
5891
6237
  async handleHttpPushEvent(payload) {
6238
+ if (!isHttpEventPayload(payload)) throw Object.assign(Error("invalid HTTP event payload"), { code: "INVALID_SYNC_INPUT" });
5892
6239
  const session = this.findSessionByBackendId(payload.sessionId);
5893
- if (!session) {
6240
+ if (!session || !this.started || this.sessionDisabled) {
5894
6241
  throw new SessionNotFoundError(payload.sessionId);
5895
6242
  }
6243
+ const assertOwner = () => {
6244
+ if (!this.started || this.sessionDisabled || this.session !== session) throw new SessionNotFoundError(payload.sessionId);
6245
+ };
5896
6246
  const normalizedMeta = parseMaybeJson(payload.metadata);
5897
6247
  const metadata = normalizedMeta && typeof normalizedMeta === "object" ? normalizedMeta : {};
5898
6248
  const input = {
@@ -5905,15 +6255,12 @@ var McpServer = class _McpServer {
5905
6255
  // orderingKey is session-scoped. Slightly coarser than the
5906
6256
  // per-conv key used by transcript emits, but acceptable because
5907
6257
  // hook-script pushes are off the hot transcript path.
5908
- timestamp: (0, import_codevibe_core5.prepareEventTimestamp)({ orderingKey: payload.sessionId })
6258
+ timestamp: (0, import_codevibe_core6.prepareEventTimestamp)({ orderingKey: payload.sessionId })
5909
6259
  };
5910
- const created = await this.createEncryptedEvent(session, input);
5911
- if (!created) {
5912
- throw new Error(
5913
- `fail-closed: encrypted session ${payload.sessionId} has no session key \u2014 event dropped (#638)`
5914
- );
5915
- }
5916
- return { eventId: created.eventId };
6260
+ return this.appSyncClient.publishCompanionEvent(input, async () => {
6261
+ assertOwner();
6262
+ return this.createEncryptedEvent(session, input);
6263
+ }, { assertOwner });
5917
6264
  }
5918
6265
  findSessionByBackendId(backendSessionId) {
5919
6266
  if (!this.session) return null;
@@ -6282,7 +6629,7 @@ async function main() {
6282
6629
  tmuxTarget: process.env.CODEVIBE_AGY_TMUX_TARGET ?? void 0,
6283
6630
  wrapperPid: process.env.CODEVIBE_AGY_WRAPPER_PID ? parseInt(process.env.CODEVIBE_AGY_WRAPPER_PID, 10) : void 0
6284
6631
  });
6285
- (0, import_codevibe_core5.installDaemonProcessGuards)(logger, {
6632
+ (0, import_codevibe_core6.installDaemonProcessGuards)(logger, {
6286
6633
  onFatal: () => {
6287
6634
  void server.stop().finally(() => process.exit(1));
6288
6635
  }