@buildautomaton/cli 0.1.26 → 0.1.28

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.
package/dist/cli.js CHANGED
@@ -25064,7 +25064,7 @@ var {
25064
25064
  } = import_index.default;
25065
25065
 
25066
25066
  // src/cli-version.ts
25067
- var CLI_VERSION = "0.1.26".length > 0 ? "0.1.26" : "0.0.0-dev";
25067
+ var CLI_VERSION = "0.1.28".length > 0 ? "0.1.28" : "0.0.0-dev";
25068
25068
 
25069
25069
  // src/cli/defaults.ts
25070
25070
  var DEFAULT_API_URL = process.env.BUILDAUTOMATON_API_URL ?? "https://api.buildautomaton.com";
@@ -25352,6 +25352,17 @@ function createCliE2eeRuntime(key) {
25352
25352
  const merged = { ...message, ...parsed };
25353
25353
  delete merged.ee;
25354
25354
  return merged;
25355
+ },
25356
+ decryptEnvelopeToBuffer(envelope) {
25357
+ if (envelope.k !== key.id) throw new Error(`E2EE key mismatch: ${envelope.k}`);
25358
+ const sealed = Buffer.from(base64UrlDecode(envelope.c));
25359
+ if (sealed.length < 16) throw new Error("Invalid E2EE payload.");
25360
+ const ciphertext = sealed.subarray(0, sealed.length - 16);
25361
+ const tag = sealed.subarray(sealed.length - 16);
25362
+ const nonce = Buffer.from(base64UrlDecode(envelope.n));
25363
+ const decipher = createDecipheriv("aes-256-gcm", rawKey, nonce);
25364
+ decipher.setAuthTag(tag);
25365
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
25355
25366
  }
25356
25367
  };
25357
25368
  }
@@ -25627,6 +25638,12 @@ function sendWsMessage(ws, payload) {
25627
25638
  }
25628
25639
  }
25629
25640
 
25641
+ // src/connection/heartbeat/constants.ts
25642
+ var BRIDGE_APP_HEARTBEAT_INTERVAL_MS = 1e4;
25643
+ var BRIDGE_HEARTBEAT_SEQ_MAX = 2147483646;
25644
+ var BRIDGE_HEARTBEAT_MISSED_ACKS_BEFORE_RECONNECT = 4;
25645
+ var BRIDGE_HEARTBEAT_RTT_SAMPLE_MAX = 5;
25646
+
25630
25647
  // ../../node_modules/.pnpm/open@10.2.0/node_modules/open/index.js
25631
25648
  import process7 from "node:process";
25632
25649
  import { Buffer as Buffer2 } from "node:buffer";
@@ -26605,14 +26622,18 @@ function runPendingAuth(options) {
26605
26622
  }
26606
26623
  function connect() {
26607
26624
  const url2 = buildPendingBridgeUrl(apiUrl, connectionId);
26625
+ let pendingHbSeq = -1;
26608
26626
  ws = createWsBridge({
26609
26627
  url: url2,
26610
26628
  onOpen: () => {
26611
26629
  clearQuietOnOpen();
26630
+ pendingHbSeq = -1;
26612
26631
  sendWsMessage(ws, { type: "identify", role: "cli", cliVersion: CLI_VERSION });
26613
26632
  keepaliveInterval = setInterval(() => {
26614
26633
  if (resolved || !ws || ws.readyState !== 1) return;
26615
- sendWsMessage(ws, { type: "ping", timestamp: Date.now() });
26634
+ pendingHbSeq = pendingHbSeq >= BRIDGE_HEARTBEAT_SEQ_MAX ? 0 : pendingHbSeq + 1;
26635
+ const hb = { t: "h", s: pendingHbSeq };
26636
+ sendWsMessage(ws, hb);
26616
26637
  }, PENDING_KEEPALIVE_MS);
26617
26638
  if (browserFallback) {
26618
26639
  clearTimeout(browserFallback);
@@ -32252,6 +32273,96 @@ function augmentPromptResultAuthFields(agentType, errorText) {
32252
32273
  return { agentAuthRequired: true, agentType };
32253
32274
  }
32254
32275
 
32276
+ // src/agents/acp/fetch-session-attachments.ts
32277
+ function metaSaysEncrypted(meta) {
32278
+ if (!meta) return false;
32279
+ const e = meta.encrypted;
32280
+ return e === true || e === "true" || e === 1;
32281
+ }
32282
+ function warnIfDecodedImageMagicUnexpected(buf, mimeType, idShort) {
32283
+ const m = mimeType.toLowerCase();
32284
+ if (!m.startsWith("image/") || buf.length < 4) return;
32285
+ const b0 = buf[0];
32286
+ const b1 = buf[1];
32287
+ const b2 = buf[2];
32288
+ const b3 = buf[3];
32289
+ let looksOk = false;
32290
+ if (m.includes("png") && b0 === 137 && b1 === 80 && b2 === 78 && b3 === 71) looksOk = true;
32291
+ else if ((m.includes("jpeg") || m.includes("jpg")) && b0 === 255 && b1 === 216 && b2 === 255) looksOk = true;
32292
+ else if (m.includes("gif") && b0 === 71 && b1 === 73 && b2 === 70) looksOk = true;
32293
+ else if (m.includes("webp") && buf.length >= 12 && buf.subarray(0, 4).toString("ascii") === "RIFF" && buf.subarray(8, 12).toString("ascii") === "WEBP")
32294
+ looksOk = true;
32295
+ else if (!/(png|jpe?g|gif|webp)/i.test(m)) looksOk = true;
32296
+ if (!looksOk) {
32297
+ logDebug(
32298
+ `[Agent] Attachment ${idShort} (${mimeType}): decoded bytes do not match common image signatures \u2014 wrong E2EE key, corrupt blob, or metadata mismatch. First 12 bytes (hex): ${buf.subarray(0, Math.min(12, buf.length)).toString("hex")}`
32299
+ );
32300
+ }
32301
+ }
32302
+ async function fetchSessionAttachmentPayloadsForAgent(params) {
32303
+ const { attachments, sessionId, cloudApiBaseUrl, getCloudAccessToken, e2ee, log: log2 } = params;
32304
+ const token = getCloudAccessToken();
32305
+ if (!token) {
32306
+ return { ok: false, error: "Missing cloud access token; cannot download attachments." };
32307
+ }
32308
+ const wantedCount = attachments.filter((a) => typeof a.attachmentId === "string" && a.attachmentId.trim() !== "").length;
32309
+ if (wantedCount === 0) {
32310
+ return { ok: false, error: "No valid attachment ids in prompt." };
32311
+ }
32312
+ const base = cloudApiBaseUrl.replace(/\/+$/, "");
32313
+ const out = [];
32314
+ for (const a of attachments) {
32315
+ const id = typeof a.attachmentId === "string" ? a.attachmentId.trim() : "";
32316
+ if (!id) continue;
32317
+ const metaUrl = `${base}/api/sessions/${encodeURIComponent(sessionId)}/attachments/${encodeURIComponent(id)}/meta`;
32318
+ const blobUrl = `${base}/api/sessions/${encodeURIComponent(sessionId)}/attachments/${encodeURIComponent(id)}/blob`;
32319
+ const metaRes = await fetch(metaUrl, { headers: { Authorization: `Bearer ${token}` } });
32320
+ if (!metaRes.ok) {
32321
+ const t = await metaRes.text().catch(() => "");
32322
+ log2(`[Agent] Attachment meta fetch failed ${metaRes.status}: ${t.slice(0, 200)}`);
32323
+ return { ok: false, error: `Could not load attachment (${id.slice(0, 8)}\u2026): ${metaRes.status}` };
32324
+ }
32325
+ const meta = await metaRes.json().catch(() => null);
32326
+ const mimeType = typeof meta?.mimeType === "string" && meta.mimeType.trim() ? meta.mimeType.trim() : a.mimeType;
32327
+ const blobRes = await fetch(blobUrl, { headers: { Authorization: `Bearer ${token}` } });
32328
+ if (!blobRes.ok) {
32329
+ const t = await blobRes.text().catch(() => "");
32330
+ log2(`[Agent] Attachment blob fetch failed ${blobRes.status}: ${t.slice(0, 200)}`);
32331
+ return { ok: false, error: `Could not load attachment data (${id.slice(0, 8)}\u2026): ${blobRes.status}` };
32332
+ }
32333
+ const buf = Buffer.from(await blobRes.arrayBuffer());
32334
+ let imageBytes;
32335
+ const encrypted = metaSaysEncrypted(meta);
32336
+ if (encrypted) {
32337
+ if (!e2ee) {
32338
+ return { ok: false, error: "Encrypted attachments require E2EE keys on this bridge." };
32339
+ }
32340
+ const k = typeof meta?.k === "string" ? meta.k : "";
32341
+ const n = typeof meta?.n === "string" ? meta.n : "";
32342
+ if (!k || !n) {
32343
+ return { ok: false, error: "Invalid encrypted attachment metadata (missing key id or nonce)." };
32344
+ }
32345
+ const c = base64UrlEncode(buf);
32346
+ imageBytes = e2ee.decryptEnvelopeToBuffer({ k, n, c });
32347
+ } else {
32348
+ imageBytes = buf;
32349
+ }
32350
+ warnIfDecodedImageMagicUnexpected(imageBytes, mimeType, id.slice(0, 8));
32351
+ logDebug(
32352
+ `[Agent] Loaded prompt image ${id.slice(0, 8)}\u2026: ${imageBytes.length} bytes, mime=${mimeType}, ${encrypted ? "E2EE decrypted" : "plaintext from storage"}`
32353
+ );
32354
+ const dataBase64 = imageBytes.toString("base64");
32355
+ out.push({ mimeType, dataBase64 });
32356
+ }
32357
+ if (out.length !== wantedCount) {
32358
+ return {
32359
+ ok: false,
32360
+ error: `Expected ${wantedCount} image attachment(s) but only loaded ${out.length} (check attachment ids and empty rows).`
32361
+ };
32362
+ }
32363
+ return { ok: true, images: out };
32364
+ }
32365
+
32255
32366
  // src/agents/acp/send-prompt-to-agent.ts
32256
32367
  async function sendPromptToAgent(options) {
32257
32368
  const {
@@ -32269,10 +32380,49 @@ async function sendPromptToAgent(options) {
32269
32380
  sessionChangeSummaryFilePaths,
32270
32381
  cloudApiBaseUrl,
32271
32382
  getCloudAccessToken,
32272
- e2ee
32383
+ e2ee,
32384
+ attachments
32273
32385
  } = options;
32274
32386
  try {
32275
- const result = await handle.sendPrompt(promptText, {});
32387
+ let sendOpts = {};
32388
+ if (attachments && attachments.length > 0) {
32389
+ if (!sessionId || !cloudApiBaseUrl || !getCloudAccessToken) {
32390
+ sendResult2({
32391
+ type: "prompt_result",
32392
+ id: promptId,
32393
+ ...sessionId ? { sessionId } : {},
32394
+ ...runId ? { runId } : {},
32395
+ success: false,
32396
+ error: "Prompt includes images but the bridge is not configured with a cloud API URL and token to fetch them.",
32397
+ ...followUpCatalogPromptId != null && followUpCatalogPromptId !== "" ? { followUpCatalogPromptId } : {},
32398
+ ...augmentPromptResultAuthFields(agentType, "missing cloud for images download")
32399
+ });
32400
+ return;
32401
+ }
32402
+ const resolved = await fetchSessionAttachmentPayloadsForAgent({
32403
+ attachments,
32404
+ sessionId,
32405
+ cloudApiBaseUrl,
32406
+ getCloudAccessToken,
32407
+ e2ee,
32408
+ log: log2
32409
+ });
32410
+ if (!resolved.ok) {
32411
+ sendResult2({
32412
+ type: "prompt_result",
32413
+ id: promptId,
32414
+ ...sessionId ? { sessionId } : {},
32415
+ ...runId ? { runId } : {},
32416
+ success: false,
32417
+ error: resolved.error,
32418
+ ...followUpCatalogPromptId != null && followUpCatalogPromptId !== "" ? { followUpCatalogPromptId } : {},
32419
+ ...augmentPromptResultAuthFields(agentType, resolved.error)
32420
+ });
32421
+ return;
32422
+ }
32423
+ sendOpts = { images: resolved.images };
32424
+ }
32425
+ const result = await handle.sendPrompt(promptText, sendOpts);
32276
32426
  if (sessionId && runId && sendSessionUpdate && agentCwd && result.success) {
32277
32427
  await collectTurnGitDiffFromPreTurnSnapshot({
32278
32428
  sessionId,
@@ -32714,13 +32864,16 @@ function parseAcpInitAgentCapabilities(initResult) {
32714
32864
  const canLoad = agentCapabilities?.loadSession === true;
32715
32865
  const sessionCaps = agentCapabilities?.sessionCapabilities;
32716
32866
  const canResume = Boolean(sessionCaps?.resume);
32717
- return { canResume, canLoad };
32867
+ const promptCaps = agentCapabilities?.promptCapabilities;
32868
+ const promptSupportsImage = promptCaps?.image === true;
32869
+ return { canResume, canLoad, promptSupportsImage };
32718
32870
  }
32719
32871
 
32720
32872
  // src/agents/acp/clients/shared/bootstrap-acp-wire-session.ts
32721
32873
  async function bootstrapAcpWireSession(transport, ctx, initializeRequest) {
32722
32874
  const initResult = await transport.initialize(initializeRequest);
32723
- const { canResume, canLoad } = parseAcpInitAgentCapabilities(initResult);
32875
+ const { canResume, canLoad, promptSupportsImage } = parseAcpInitAgentCapabilities(initResult);
32876
+ ctx.agentPromptImageSupported = promptSupportsImage;
32724
32877
  await transport.afterInitialize?.();
32725
32878
  const established = await establishAcpSessionWithTransport(transport, ctx, canResume, canLoad);
32726
32879
  const sessionId = established.sessionId;
@@ -32811,11 +32964,27 @@ function normalizeAcpPromptTurnFailure(err, stderrCaptureText) {
32811
32964
  }
32812
32965
 
32813
32966
  // src/agents/acp/clients/shared/send-acp-prompt-via-transport.ts
32814
- async function sendAcpPromptViaTransport(transport, ctx, sessionId, promptText) {
32967
+ async function sendAcpPromptViaTransport(transport, ctx, sessionId, promptText, images) {
32968
+ if (images && images.length > 0 && !ctx.agentPromptImageSupported) {
32969
+ await new Promise((r) => setImmediate(r));
32970
+ return normalizeAcpPromptTurnFailure(
32971
+ new Error(
32972
+ "This agent does not advertise image support in ACP (missing agentCapabilities.promptCapabilities.image). Remove images or use an agent that supports prompt images."
32973
+ ),
32974
+ ctx.getStderrText()
32975
+ );
32976
+ }
32977
+ const textBlock = promptText.trim() !== "" ? promptText : images?.length ? " " : "";
32978
+ const prompt = [{ type: "text", text: textBlock }];
32979
+ if (images && images.length > 0) {
32980
+ for (const im of images) {
32981
+ prompt.push({ type: "image", mimeType: im.mimeType, data: im.data });
32982
+ }
32983
+ }
32815
32984
  try {
32816
32985
  const response = await transport.prompt({
32817
32986
  sessionId,
32818
- prompt: [{ type: "text", text: promptText }]
32987
+ prompt
32819
32988
  });
32820
32989
  await new Promise((r2) => setImmediate(r2));
32821
32990
  const r = response;
@@ -32991,15 +33160,17 @@ async function createSdkStdioAcpClient(options) {
32991
33160
  const established = await bootstrapAcpWireSession(transport, sessionCtx, {
32992
33161
  protocolVersion: PROTOCOL_VERSION2,
32993
33162
  clientCapabilities: {
32994
- fs: { readTextFile: true, writeTextFile: true }
33163
+ fs: { readTextFile: true, writeTextFile: true },
33164
+ prompt: { image: true }
32995
33165
  },
32996
33166
  clientInfo: { name: "buildautomaton-cli", version: "0.1.0" }
32997
33167
  });
32998
33168
  const sessionId = established.sessionId;
32999
33169
  settleResolve({
33000
33170
  sessionId,
33001
- async sendPrompt(prompt, _options) {
33002
- return sendAcpPromptViaTransport(transport, sessionCtx, sessionId, prompt);
33171
+ async sendPrompt(prompt, options2) {
33172
+ const imgs = options2?.images?.map((im) => ({ type: "image", mimeType: im.mimeType, data: im.dataBase64 }));
33173
+ return sendAcpPromptViaTransport(transport, sessionCtx, sessionId, prompt, imgs);
33003
33174
  },
33004
33175
  async cancel() {
33005
33176
  for (const [id, entry] of [...pendingPermissionReplies.entries()]) {
@@ -33367,14 +33538,19 @@ async function createCursorAcpClient(options) {
33367
33538
  });
33368
33539
  const established = await bootstrapAcpWireSession(transport, sessionCtx, {
33369
33540
  protocolVersion: 1,
33370
- clientCapabilities: { fs: { readTextFile: true, writeTextFile: true }, terminal: false },
33541
+ clientCapabilities: {
33542
+ fs: { readTextFile: true, writeTextFile: true },
33543
+ terminal: false,
33544
+ prompt: { image: true }
33545
+ },
33371
33546
  clientInfo: { name: "buildautomaton-cli", version: "0.1.0" }
33372
33547
  });
33373
33548
  const sessionId = established.sessionId;
33374
33549
  resolve18({
33375
33550
  sessionId,
33376
- async sendPrompt(prompt, _options) {
33377
- return sendAcpPromptViaTransport(transport, sessionCtx, sessionId, prompt);
33551
+ async sendPrompt(prompt, options2) {
33552
+ const imgs = options2?.images?.map((im) => ({ type: "image", mimeType: im.mimeType, data: im.dataBase64 }));
33553
+ return sendAcpPromptViaTransport(transport, sessionCtx, sessionId, prompt, imgs);
33378
33554
  },
33379
33555
  async cancel() {
33380
33556
  cancelPendingPermissionRequests2();
@@ -34405,7 +34581,8 @@ async function createAcpManager(options) {
34405
34581
  sessionChangeSummaryFilePaths,
34406
34582
  cloudApiBaseUrl,
34407
34583
  getCloudAccessToken,
34408
- e2ee
34584
+ e2ee,
34585
+ attachments
34409
34586
  } = opts;
34410
34587
  const preferredForPrompt = agentType ?? backendFallbackAgentType ?? null;
34411
34588
  pendingCancelRunId = void 0;
@@ -34476,7 +34653,8 @@ async function createAcpManager(options) {
34476
34653
  sessionChangeSummaryFilePaths,
34477
34654
  cloudApiBaseUrl,
34478
34655
  getCloudAccessToken,
34479
- e2ee
34656
+ e2ee,
34657
+ attachments
34480
34658
  });
34481
34659
  }
34482
34660
  void run().finally(() => {
@@ -37614,6 +37792,7 @@ function reportGitRepos(getWs, log2) {
37614
37792
  var API_TO_BRIDGE_MESSAGE_TYPES = [
37615
37793
  "auth_token",
37616
37794
  "bridge_identified",
37795
+ "ha",
37617
37796
  "dev_servers_config",
37618
37797
  "server_control",
37619
37798
  "agent_config",
@@ -37642,6 +37821,10 @@ function parseApiToBridgeMessage(data, log2) {
37642
37821
  }
37643
37822
  return null;
37644
37823
  }
37824
+ if (t === "ha") {
37825
+ const s = data.s;
37826
+ if (typeof s !== "number" || !Number.isFinite(s)) return null;
37827
+ }
37645
37828
  return data;
37646
37829
  }
37647
37830
 
@@ -37682,6 +37865,13 @@ var handleBridgeIdentified = (msg, deps) => {
37682
37865
  });
37683
37866
  };
37684
37867
 
37868
+ // src/connection/heartbeat/ack.ts
37869
+ var handleBridgeHeartbeatAck = (msg, deps) => {
37870
+ const raw = msg.s;
37871
+ if (typeof raw !== "number" || !Number.isFinite(raw)) return;
37872
+ deps.onBridgeHeartbeatAck?.(Math.trunc(raw));
37873
+ };
37874
+
37685
37875
  // src/agents/acp/from-bridge/handle-bridge-agent-config.ts
37686
37876
  function handleBridgeAgentConfig(msg, { acpManager }) {
37687
37877
  if (!Array.isArray(msg.agents) || msg.agents.length === 0) return;
@@ -37848,7 +38038,8 @@ function dispatchLocalPrompt(next, deps) {
37848
38038
  ...Array.isArray(pl.sessionChangeSummaryFilePaths) ? { sessionChangeSummaryFilePaths: pl.sessionChangeSummaryFilePaths } : {},
37849
38039
  ...Array.isArray(pl.sessionChangeSummaryFileSnapshots) ? { sessionChangeSummaryFileSnapshots: pl.sessionChangeSummaryFileSnapshots } : {},
37850
38040
  ...typeof pl.agentType === "string" && pl.agentType.trim() ? { agentType: pl.agentType.trim() } : {},
37851
- ...pl.agentConfig != null && typeof pl.agentConfig === "object" && !Array.isArray(pl.agentConfig) && Object.keys(pl.agentConfig).length > 0 ? { agentConfig: pl.agentConfig } : {}
38041
+ ...pl.agentConfig != null && typeof pl.agentConfig === "object" && !Array.isArray(pl.agentConfig) && Object.keys(pl.agentConfig).length > 0 ? { agentConfig: pl.agentConfig } : {},
38042
+ ...Array.isArray(pl.attachments) && pl.attachments.length > 0 ? { attachments: pl.attachments } : {}
37852
38043
  };
37853
38044
  handleBridgePrompt(msg, deps);
37854
38045
  }
@@ -38115,15 +38306,30 @@ function resolveChangeSummaryPromptForAgent(params) {
38115
38306
  }
38116
38307
 
38117
38308
  // src/agents/acp/from-bridge/handle-bridge-prompt.ts
38309
+ function parseBridgeAttachments(msg) {
38310
+ const raw = msg.attachments;
38311
+ if (!Array.isArray(raw)) return [];
38312
+ const out = [];
38313
+ for (const x of raw) {
38314
+ if (x === null || typeof x !== "object" || Array.isArray(x)) continue;
38315
+ const o = x;
38316
+ const id = typeof o.attachmentId === "string" ? o.attachmentId.trim() : "";
38317
+ if (!id) continue;
38318
+ const mt = typeof o.mimeType === "string" && o.mimeType.trim() ? o.mimeType.trim() : "application/octet-stream";
38319
+ out.push({ attachmentId: id, mimeType: mt });
38320
+ }
38321
+ return out;
38322
+ }
38118
38323
  function handleBridgePrompt(msg, deps) {
38119
38324
  const { getWs, log: log2, acpManager, sessionWorktreeManager } = deps;
38120
38325
  const rawPrompt = msg.prompt;
38121
38326
  const promptText = typeof rawPrompt === "string" ? rawPrompt : rawPrompt != null ? String(rawPrompt) : "";
38327
+ const attachments = parseBridgeAttachments(msg);
38122
38328
  const sessionId = msg.sessionId;
38123
38329
  const runId = typeof msg.runId === "string" ? msg.runId : void 0;
38124
38330
  const promptId = typeof msg.id === "string" ? msg.id : void 0;
38125
38331
  const { sendBridgeMessage, sendResult: sendResult2, sendSessionUpdate } = createBridgePromptSenders(deps, getWs);
38126
- if (!promptText.trim()) {
38332
+ if (!promptText.trim() && attachments.length === 0) {
38127
38333
  log2(
38128
38334
  `[Bridge service] Prompt ignored: empty or missing prompt text (session ${typeof msg.sessionId === "string" ? msg.sessionId.slice(0, 8) : "\u2014"}\u2026, run ${typeof msg.runId === "string" ? msg.runId.slice(0, 8) : "\u2014"}\u2026).`
38129
38335
  );
@@ -38190,7 +38396,8 @@ function handleBridgePrompt(msg, deps) {
38190
38396
  sessionChangeSummaryFilePaths: pathsForUpload,
38191
38397
  cloudApiBaseUrl: deps.cloudApiBaseUrl,
38192
38398
  getCloudAccessToken: deps.getCloudAccessToken,
38193
- e2ee: deps.e2ee
38399
+ e2ee: deps.e2ee,
38400
+ ...attachments.length > 0 ? { attachments } : {}
38194
38401
  });
38195
38402
  }
38196
38403
  void sessionWorktreeManager.resolveSessionParentPathForPrompt(sessionId, { isNewSession, sessionParent, sessionParentPath }).then((cwd) => preambleAndPrompt(cwd)).catch((err) => {
@@ -39008,6 +39215,9 @@ function dispatchBridgeMessage(msg, deps) {
39008
39215
  case "bridge_identified":
39009
39216
  handleBridgeIdentified(msg, deps);
39010
39217
  break;
39218
+ case "ha":
39219
+ handleBridgeHeartbeatAck(msg, deps);
39220
+ break;
39011
39221
  case "dev_servers_config":
39012
39222
  handleDevServersConfig(msg, deps);
39013
39223
  break;
@@ -39063,9 +39273,17 @@ function dispatchBridgeMessage(msg, deps) {
39063
39273
  }
39064
39274
 
39065
39275
  // src/routing/handle-bridge-message.ts
39276
+ function normalizeInboundBridgeWebSocketJson(data) {
39277
+ if (data === null || typeof data !== "object" || Array.isArray(data)) return data;
39278
+ const o = data;
39279
+ if (o.t === "ha" && typeof o.s === "number" && Number.isFinite(o.s)) {
39280
+ return { type: "ha", s: Math.trunc(o.s) };
39281
+ }
39282
+ return data;
39283
+ }
39066
39284
  function handleBridgeMessage(data, deps) {
39067
39285
  if (!deps.getWs()) return;
39068
- const msg = parseApiToBridgeMessage(data, deps.log);
39286
+ const msg = parseApiToBridgeMessage(normalizeInboundBridgeWebSocketJson(data), deps.log);
39069
39287
  if (!msg) return;
39070
39288
  setImmediate(() => {
39071
39289
  dispatchBridgeMessage(msg, deps);
@@ -39113,7 +39331,8 @@ function createMainBridgeWebSocketLifecycle(params) {
39113
39331
  persistTokens,
39114
39332
  onAuthInvalid,
39115
39333
  e2ee,
39116
- identifyReportedPaths
39334
+ identifyReportedPaths,
39335
+ bridgeHeartbeat
39117
39336
  } = params;
39118
39337
  let authRefreshInFlight = false;
39119
39338
  function handleOpen() {
@@ -39145,6 +39364,7 @@ function createMainBridgeWebSocketLifecycle(params) {
39145
39364
  }
39146
39365
  }
39147
39366
  function handleClose(code, reason) {
39367
+ bridgeHeartbeat?.stop();
39148
39368
  try {
39149
39369
  const was = state.currentWs;
39150
39370
  state.currentWs = null;
@@ -39179,6 +39399,7 @@ function createMainBridgeWebSocketLifecycle(params) {
39179
39399
  } catch {
39180
39400
  }
39181
39401
  }
39402
+ bridgeHeartbeat?.stop();
39182
39403
  const prev = state.currentWs;
39183
39404
  if (prev) {
39184
39405
  prev.removeAllListeners();
@@ -39270,6 +39491,92 @@ function createMainBridgeWebSocketLifecycle(params) {
39270
39491
  return { connect };
39271
39492
  }
39272
39493
 
39494
+ // src/connection/heartbeat/controller.ts
39495
+ function meanRttMs(samples) {
39496
+ if (samples.length === 0) return void 0;
39497
+ let sum = 0;
39498
+ for (const x of samples) sum += x;
39499
+ return sum / samples.length;
39500
+ }
39501
+ function createBridgeHeartbeatController(params) {
39502
+ const { getWs, log: log2 } = params;
39503
+ let interval = null;
39504
+ let seqCursor = -1;
39505
+ let awaitingSeq = null;
39506
+ let sentAtMs = 0;
39507
+ let missed = 0;
39508
+ const rttSamples = [];
39509
+ function clearTimer() {
39510
+ if (interval != null) {
39511
+ clearInterval(interval);
39512
+ interval = null;
39513
+ }
39514
+ }
39515
+ function nextSeq() {
39516
+ seqCursor = seqCursor >= BRIDGE_HEARTBEAT_SEQ_MAX ? 0 : seqCursor + 1;
39517
+ return seqCursor;
39518
+ }
39519
+ function tick() {
39520
+ const ws = getWs();
39521
+ if (!ws || ws.readyState !== wrapper_default.OPEN) return;
39522
+ if (awaitingSeq !== null) {
39523
+ missed++;
39524
+ if (missed >= BRIDGE_HEARTBEAT_MISSED_ACKS_BEFORE_RECONNECT) {
39525
+ try {
39526
+ log2("[Bridge service] Heartbeat missed repeatedly; reconnecting\u2026");
39527
+ } catch {
39528
+ }
39529
+ clearTimer();
39530
+ awaitingSeq = null;
39531
+ missed = 0;
39532
+ rttSamples.length = 0;
39533
+ safeCloseWebSocket(ws);
39534
+ return;
39535
+ }
39536
+ }
39537
+ const seq = nextSeq();
39538
+ const mean = meanRttMs(rttSamples);
39539
+ const payload = mean !== void 0 && Number.isFinite(mean) ? { t: "h", s: seq, m: Math.round(mean) } : { t: "h", s: seq };
39540
+ sendWsMessage(ws, payload);
39541
+ awaitingSeq = seq;
39542
+ sentAtMs = Date.now();
39543
+ }
39544
+ return {
39545
+ start() {
39546
+ clearTimer();
39547
+ awaitingSeq = null;
39548
+ missed = 0;
39549
+ seqCursor = -1;
39550
+ rttSamples.length = 0;
39551
+ interval = setInterval(tick, BRIDGE_APP_HEARTBEAT_INTERVAL_MS);
39552
+ },
39553
+ stop() {
39554
+ clearTimer();
39555
+ awaitingSeq = null;
39556
+ missed = 0;
39557
+ rttSamples.length = 0;
39558
+ },
39559
+ onAck(seq) {
39560
+ if (awaitingSeq === null) return;
39561
+ if (!Number.isFinite(seq)) return;
39562
+ const ack = Math.trunc(seq);
39563
+ const pending = awaitingSeq;
39564
+ if (ack < pending) return;
39565
+ if (ack === pending) {
39566
+ const rtt = Date.now() - sentAtMs;
39567
+ if (Number.isFinite(rtt) && rtt >= 0 && rtt < 6e5) {
39568
+ rttSamples.push(rtt);
39569
+ if (rttSamples.length > BRIDGE_HEARTBEAT_RTT_SAMPLE_MAX) {
39570
+ rttSamples.splice(0, rttSamples.length - BRIDGE_HEARTBEAT_RTT_SAMPLE_MAX);
39571
+ }
39572
+ }
39573
+ }
39574
+ awaitingSeq = null;
39575
+ missed = 0;
39576
+ }
39577
+ };
39578
+ }
39579
+
39273
39580
  // src/connection/create-bridge-connection.ts
39274
39581
  async function createBridgeConnection(options) {
39275
39582
  const { apiUrl, workspaceId, justAuthenticated, onAuthInvalid, persistTokens } = options;
@@ -39309,13 +39616,18 @@ async function createBridgeConnection(options) {
39309
39616
  }
39310
39617
  const e2ee = options.e2eCertificate ? createCliE2eeRuntime(options.e2eCertificate) : void 0;
39311
39618
  const devServerManager = new DevServerManager({ getWs, log: logFn, getBridgeRoot, e2ee });
39312
- const onBridgeIdentified = createOnBridgeIdentified({
39619
+ const bridgeHeartbeat = createBridgeHeartbeatController({ getWs, log: logFn });
39620
+ const baseOnBridgeIdentified = createOnBridgeIdentified({
39313
39621
  devServerManager,
39314
39622
  firehoseServerUrl,
39315
39623
  workspaceId,
39316
39624
  state,
39317
39625
  logFn
39318
39626
  });
39627
+ const onBridgeIdentified = (msg) => {
39628
+ baseOnBridgeIdentified(msg);
39629
+ bridgeHeartbeat.start();
39630
+ };
39319
39631
  const sendLocalSkillsReport = createSendLocalSkillsReport(getWs, logFn);
39320
39632
  const reportAutoDetectedAgents = createReportAutoDetectedAgents(getWs, logFn);
39321
39633
  const messageDeps = {
@@ -39324,6 +39636,9 @@ async function createBridgeConnection(options) {
39324
39636
  acpManager,
39325
39637
  sessionWorktreeManager,
39326
39638
  onBridgeIdentified,
39639
+ onBridgeHeartbeatAck: (seq) => {
39640
+ bridgeHeartbeat.onAck(seq);
39641
+ },
39327
39642
  sendLocalSkillsReport,
39328
39643
  reportAutoDetectedAgents,
39329
39644
  devServerManager,
@@ -39347,13 +39662,15 @@ async function createBridgeConnection(options) {
39347
39662
  persistTokens,
39348
39663
  onAuthInvalid,
39349
39664
  e2ee,
39350
- identifyReportedPaths
39665
+ identifyReportedPaths,
39666
+ bridgeHeartbeat
39351
39667
  });
39352
39668
  connect();
39353
39669
  const stopFileIndexWatcher = startFileIndexWatcher(getBridgeRoot());
39354
39670
  return {
39355
39671
  close: async () => {
39356
39672
  stopFileIndexWatcher();
39673
+ bridgeHeartbeat.stop();
39357
39674
  await closeBridgeConnection(state, acpManager, devServerManager, logFn);
39358
39675
  }
39359
39676
  };