@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/index.js CHANGED
@@ -23128,13 +23128,16 @@ function parseAcpInitAgentCapabilities(initResult) {
23128
23128
  const canLoad = agentCapabilities?.loadSession === true;
23129
23129
  const sessionCaps = agentCapabilities?.sessionCapabilities;
23130
23130
  const canResume = Boolean(sessionCaps?.resume);
23131
- return { canResume, canLoad };
23131
+ const promptCaps = agentCapabilities?.promptCapabilities;
23132
+ const promptSupportsImage = promptCaps?.image === true;
23133
+ return { canResume, canLoad, promptSupportsImage };
23132
23134
  }
23133
23135
 
23134
23136
  // src/agents/acp/clients/shared/bootstrap-acp-wire-session.ts
23135
23137
  async function bootstrapAcpWireSession(transport, ctx, initializeRequest) {
23136
23138
  const initResult = await transport.initialize(initializeRequest);
23137
- const { canResume, canLoad } = parseAcpInitAgentCapabilities(initResult);
23139
+ const { canResume, canLoad, promptSupportsImage } = parseAcpInitAgentCapabilities(initResult);
23140
+ ctx.agentPromptImageSupported = promptSupportsImage;
23138
23141
  await transport.afterInitialize?.();
23139
23142
  const established = await establishAcpSessionWithTransport(transport, ctx, canResume, canLoad);
23140
23143
  const sessionId = established.sessionId;
@@ -23225,11 +23228,27 @@ function normalizeAcpPromptTurnFailure(err, stderrCaptureText) {
23225
23228
  }
23226
23229
 
23227
23230
  // src/agents/acp/clients/shared/send-acp-prompt-via-transport.ts
23228
- async function sendAcpPromptViaTransport(transport, ctx, sessionId, promptText) {
23231
+ async function sendAcpPromptViaTransport(transport, ctx, sessionId, promptText, images) {
23232
+ if (images && images.length > 0 && !ctx.agentPromptImageSupported) {
23233
+ await new Promise((r) => setImmediate(r));
23234
+ return normalizeAcpPromptTurnFailure(
23235
+ new Error(
23236
+ "This agent does not advertise image support in ACP (missing agentCapabilities.promptCapabilities.image). Remove images or use an agent that supports prompt images."
23237
+ ),
23238
+ ctx.getStderrText()
23239
+ );
23240
+ }
23241
+ const textBlock = promptText.trim() !== "" ? promptText : images?.length ? " " : "";
23242
+ const prompt = [{ type: "text", text: textBlock }];
23243
+ if (images && images.length > 0) {
23244
+ for (const im of images) {
23245
+ prompt.push({ type: "image", mimeType: im.mimeType, data: im.data });
23246
+ }
23247
+ }
23229
23248
  try {
23230
23249
  const response = await transport.prompt({
23231
23250
  sessionId,
23232
- prompt: [{ type: "text", text: promptText }]
23251
+ prompt
23233
23252
  });
23234
23253
  await new Promise((r2) => setImmediate(r2));
23235
23254
  const r = response;
@@ -23405,15 +23424,17 @@ async function createSdkStdioAcpClient(options) {
23405
23424
  const established = await bootstrapAcpWireSession(transport, sessionCtx, {
23406
23425
  protocolVersion: PROTOCOL_VERSION2,
23407
23426
  clientCapabilities: {
23408
- fs: { readTextFile: true, writeTextFile: true }
23427
+ fs: { readTextFile: true, writeTextFile: true },
23428
+ prompt: { image: true }
23409
23429
  },
23410
23430
  clientInfo: { name: "buildautomaton-cli", version: "0.1.0" }
23411
23431
  });
23412
23432
  const sessionId = established.sessionId;
23413
23433
  settleResolve({
23414
23434
  sessionId,
23415
- async sendPrompt(prompt, _options) {
23416
- return sendAcpPromptViaTransport(transport, sessionCtx, sessionId, prompt);
23435
+ async sendPrompt(prompt, options2) {
23436
+ const imgs = options2?.images?.map((im) => ({ type: "image", mimeType: im.mimeType, data: im.dataBase64 }));
23437
+ return sendAcpPromptViaTransport(transport, sessionCtx, sessionId, prompt, imgs);
23417
23438
  },
23418
23439
  async cancel() {
23419
23440
  for (const [id, entry] of [...pendingPermissionReplies.entries()]) {
@@ -23875,7 +23896,13 @@ function installBridgeProcessResilience() {
23875
23896
  }
23876
23897
 
23877
23898
  // src/cli-version.ts
23878
- var CLI_VERSION = "0.1.26".length > 0 ? "0.1.26" : "0.0.0-dev";
23899
+ var CLI_VERSION = "0.1.28".length > 0 ? "0.1.28" : "0.0.0-dev";
23900
+
23901
+ // src/connection/heartbeat/constants.ts
23902
+ var BRIDGE_APP_HEARTBEAT_INTERVAL_MS = 1e4;
23903
+ var BRIDGE_HEARTBEAT_SEQ_MAX = 2147483646;
23904
+ var BRIDGE_HEARTBEAT_MISSED_ACKS_BEFORE_RECONNECT = 4;
23905
+ var BRIDGE_HEARTBEAT_RTT_SAMPLE_MAX = 5;
23879
23906
 
23880
23907
  // ../../node_modules/.pnpm/open@10.2.0/node_modules/open/index.js
23881
23908
  import process7 from "node:process";
@@ -24855,14 +24882,18 @@ function runPendingAuth(options) {
24855
24882
  }
24856
24883
  function connect() {
24857
24884
  const url2 = buildPendingBridgeUrl(apiUrl, connectionId);
24885
+ let pendingHbSeq = -1;
24858
24886
  ws = createWsBridge({
24859
24887
  url: url2,
24860
24888
  onOpen: () => {
24861
24889
  clearQuietOnOpen();
24890
+ pendingHbSeq = -1;
24862
24891
  sendWsMessage(ws, { type: "identify", role: "cli", cliVersion: CLI_VERSION });
24863
24892
  keepaliveInterval = setInterval(() => {
24864
24893
  if (resolved || !ws || ws.readyState !== 1) return;
24865
- sendWsMessage(ws, { type: "ping", timestamp: Date.now() });
24894
+ pendingHbSeq = pendingHbSeq >= BRIDGE_HEARTBEAT_SEQ_MAX ? 0 : pendingHbSeq + 1;
24895
+ const hb = { t: "h", s: pendingHbSeq };
24896
+ sendWsMessage(ws, hb);
24866
24897
  }, PENDING_KEEPALIVE_MS);
24867
24898
  if (browserFallback) {
24868
24899
  clearTimeout(browserFallback);
@@ -29951,6 +29982,122 @@ function augmentPromptResultAuthFields(agentType, errorText) {
29951
29982
  return { agentAuthRequired: true, agentType };
29952
29983
  }
29953
29984
 
29985
+ // ../e2ee/src/constants.ts
29986
+ var E2EE_NONCE_BYTES = 12;
29987
+
29988
+ // ../e2ee/src/types.ts
29989
+ function isE2eeEnvelope(value) {
29990
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
29991
+ const o = value;
29992
+ return typeof o.k === "string" && typeof o.n === "string" && typeof o.c === "string";
29993
+ }
29994
+
29995
+ // ../e2ee/src/encoding.ts
29996
+ function base64UrlEncode(bytes) {
29997
+ let binary = "";
29998
+ for (let i = 0; i < bytes.length; i += 1) binary += String.fromCharCode(bytes[i]);
29999
+ const b64 = btoa(binary);
30000
+ return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
30001
+ }
30002
+ function base64UrlDecode(value) {
30003
+ const b64 = value.replace(/-/g, "+").replace(/_/g, "/");
30004
+ const padded = b64 + "=".repeat((4 - b64.length % 4) % 4);
30005
+ const binary = atob(padded);
30006
+ const out = new Uint8Array(binary.length);
30007
+ for (let i = 0; i < binary.length; i += 1) out[i] = binary.charCodeAt(i);
30008
+ return out;
30009
+ }
30010
+
30011
+ // src/agents/acp/fetch-session-attachments.ts
30012
+ function metaSaysEncrypted(meta) {
30013
+ if (!meta) return false;
30014
+ const e = meta.encrypted;
30015
+ return e === true || e === "true" || e === 1;
30016
+ }
30017
+ function warnIfDecodedImageMagicUnexpected(buf, mimeType, idShort) {
30018
+ const m = mimeType.toLowerCase();
30019
+ if (!m.startsWith("image/") || buf.length < 4) return;
30020
+ const b0 = buf[0];
30021
+ const b1 = buf[1];
30022
+ const b2 = buf[2];
30023
+ const b3 = buf[3];
30024
+ let looksOk = false;
30025
+ if (m.includes("png") && b0 === 137 && b1 === 80 && b2 === 78 && b3 === 71) looksOk = true;
30026
+ else if ((m.includes("jpeg") || m.includes("jpg")) && b0 === 255 && b1 === 216 && b2 === 255) looksOk = true;
30027
+ else if (m.includes("gif") && b0 === 71 && b1 === 73 && b2 === 70) looksOk = true;
30028
+ else if (m.includes("webp") && buf.length >= 12 && buf.subarray(0, 4).toString("ascii") === "RIFF" && buf.subarray(8, 12).toString("ascii") === "WEBP")
30029
+ looksOk = true;
30030
+ else if (!/(png|jpe?g|gif|webp)/i.test(m)) looksOk = true;
30031
+ if (!looksOk) {
30032
+ logDebug(
30033
+ `[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")}`
30034
+ );
30035
+ }
30036
+ }
30037
+ async function fetchSessionAttachmentPayloadsForAgent(params) {
30038
+ const { attachments, sessionId, cloudApiBaseUrl, getCloudAccessToken, e2ee, log: log2 } = params;
30039
+ const token = getCloudAccessToken();
30040
+ if (!token) {
30041
+ return { ok: false, error: "Missing cloud access token; cannot download attachments." };
30042
+ }
30043
+ const wantedCount = attachments.filter((a) => typeof a.attachmentId === "string" && a.attachmentId.trim() !== "").length;
30044
+ if (wantedCount === 0) {
30045
+ return { ok: false, error: "No valid attachment ids in prompt." };
30046
+ }
30047
+ const base = cloudApiBaseUrl.replace(/\/+$/, "");
30048
+ const out = [];
30049
+ for (const a of attachments) {
30050
+ const id = typeof a.attachmentId === "string" ? a.attachmentId.trim() : "";
30051
+ if (!id) continue;
30052
+ const metaUrl = `${base}/api/sessions/${encodeURIComponent(sessionId)}/attachments/${encodeURIComponent(id)}/meta`;
30053
+ const blobUrl = `${base}/api/sessions/${encodeURIComponent(sessionId)}/attachments/${encodeURIComponent(id)}/blob`;
30054
+ const metaRes = await fetch(metaUrl, { headers: { Authorization: `Bearer ${token}` } });
30055
+ if (!metaRes.ok) {
30056
+ const t = await metaRes.text().catch(() => "");
30057
+ log2(`[Agent] Attachment meta fetch failed ${metaRes.status}: ${t.slice(0, 200)}`);
30058
+ return { ok: false, error: `Could not load attachment (${id.slice(0, 8)}\u2026): ${metaRes.status}` };
30059
+ }
30060
+ const meta = await metaRes.json().catch(() => null);
30061
+ const mimeType = typeof meta?.mimeType === "string" && meta.mimeType.trim() ? meta.mimeType.trim() : a.mimeType;
30062
+ const blobRes = await fetch(blobUrl, { headers: { Authorization: `Bearer ${token}` } });
30063
+ if (!blobRes.ok) {
30064
+ const t = await blobRes.text().catch(() => "");
30065
+ log2(`[Agent] Attachment blob fetch failed ${blobRes.status}: ${t.slice(0, 200)}`);
30066
+ return { ok: false, error: `Could not load attachment data (${id.slice(0, 8)}\u2026): ${blobRes.status}` };
30067
+ }
30068
+ const buf = Buffer.from(await blobRes.arrayBuffer());
30069
+ let imageBytes;
30070
+ const encrypted = metaSaysEncrypted(meta);
30071
+ if (encrypted) {
30072
+ if (!e2ee) {
30073
+ return { ok: false, error: "Encrypted attachments require E2EE keys on this bridge." };
30074
+ }
30075
+ const k = typeof meta?.k === "string" ? meta.k : "";
30076
+ const n = typeof meta?.n === "string" ? meta.n : "";
30077
+ if (!k || !n) {
30078
+ return { ok: false, error: "Invalid encrypted attachment metadata (missing key id or nonce)." };
30079
+ }
30080
+ const c = base64UrlEncode(buf);
30081
+ imageBytes = e2ee.decryptEnvelopeToBuffer({ k, n, c });
30082
+ } else {
30083
+ imageBytes = buf;
30084
+ }
30085
+ warnIfDecodedImageMagicUnexpected(imageBytes, mimeType, id.slice(0, 8));
30086
+ logDebug(
30087
+ `[Agent] Loaded prompt image ${id.slice(0, 8)}\u2026: ${imageBytes.length} bytes, mime=${mimeType}, ${encrypted ? "E2EE decrypted" : "plaintext from storage"}`
30088
+ );
30089
+ const dataBase64 = imageBytes.toString("base64");
30090
+ out.push({ mimeType, dataBase64 });
30091
+ }
30092
+ if (out.length !== wantedCount) {
30093
+ return {
30094
+ ok: false,
30095
+ error: `Expected ${wantedCount} image attachment(s) but only loaded ${out.length} (check attachment ids and empty rows).`
30096
+ };
30097
+ }
30098
+ return { ok: true, images: out };
30099
+ }
30100
+
29954
30101
  // src/agents/acp/send-prompt-to-agent.ts
29955
30102
  async function sendPromptToAgent(options) {
29956
30103
  const {
@@ -29968,10 +30115,49 @@ async function sendPromptToAgent(options) {
29968
30115
  sessionChangeSummaryFilePaths,
29969
30116
  cloudApiBaseUrl,
29970
30117
  getCloudAccessToken,
29971
- e2ee
30118
+ e2ee,
30119
+ attachments
29972
30120
  } = options;
29973
30121
  try {
29974
- const result = await handle.sendPrompt(promptText, {});
30122
+ let sendOpts = {};
30123
+ if (attachments && attachments.length > 0) {
30124
+ if (!sessionId || !cloudApiBaseUrl || !getCloudAccessToken) {
30125
+ sendResult2({
30126
+ type: "prompt_result",
30127
+ id: promptId,
30128
+ ...sessionId ? { sessionId } : {},
30129
+ ...runId ? { runId } : {},
30130
+ success: false,
30131
+ error: "Prompt includes images but the bridge is not configured with a cloud API URL and token to fetch them.",
30132
+ ...followUpCatalogPromptId != null && followUpCatalogPromptId !== "" ? { followUpCatalogPromptId } : {},
30133
+ ...augmentPromptResultAuthFields(agentType, "missing cloud for images download")
30134
+ });
30135
+ return;
30136
+ }
30137
+ const resolved = await fetchSessionAttachmentPayloadsForAgent({
30138
+ attachments,
30139
+ sessionId,
30140
+ cloudApiBaseUrl,
30141
+ getCloudAccessToken,
30142
+ e2ee,
30143
+ log: log2
30144
+ });
30145
+ if (!resolved.ok) {
30146
+ sendResult2({
30147
+ type: "prompt_result",
30148
+ id: promptId,
30149
+ ...sessionId ? { sessionId } : {},
30150
+ ...runId ? { runId } : {},
30151
+ success: false,
30152
+ error: resolved.error,
30153
+ ...followUpCatalogPromptId != null && followUpCatalogPromptId !== "" ? { followUpCatalogPromptId } : {},
30154
+ ...augmentPromptResultAuthFields(agentType, resolved.error)
30155
+ });
30156
+ return;
30157
+ }
30158
+ sendOpts = { images: resolved.images };
30159
+ }
30160
+ const result = await handle.sendPrompt(promptText, sendOpts);
29975
30161
  if (sessionId && runId && sendSessionUpdate && agentCwd && result.success) {
29976
30162
  await collectTurnGitDiffFromPreTurnSnapshot({
29977
30163
  sessionId,
@@ -30392,14 +30578,19 @@ async function createCursorAcpClient(options) {
30392
30578
  });
30393
30579
  const established = await bootstrapAcpWireSession(transport, sessionCtx, {
30394
30580
  protocolVersion: 1,
30395
- clientCapabilities: { fs: { readTextFile: true, writeTextFile: true }, terminal: false },
30581
+ clientCapabilities: {
30582
+ fs: { readTextFile: true, writeTextFile: true },
30583
+ terminal: false,
30584
+ prompt: { image: true }
30585
+ },
30396
30586
  clientInfo: { name: "buildautomaton-cli", version: "0.1.0" }
30397
30587
  });
30398
30588
  const sessionId = established.sessionId;
30399
30589
  resolve16({
30400
30590
  sessionId,
30401
- async sendPrompt(prompt, _options) {
30402
- return sendAcpPromptViaTransport(transport, sessionCtx, sessionId, prompt);
30591
+ async sendPrompt(prompt, options2) {
30592
+ const imgs = options2?.images?.map((im) => ({ type: "image", mimeType: im.mimeType, data: im.dataBase64 }));
30593
+ return sendAcpPromptViaTransport(transport, sessionCtx, sessionId, prompt, imgs);
30403
30594
  },
30404
30595
  async cancel() {
30405
30596
  cancelPendingPermissionRequests2();
@@ -31430,7 +31621,8 @@ async function createAcpManager(options) {
31430
31621
  sessionChangeSummaryFilePaths,
31431
31622
  cloudApiBaseUrl,
31432
31623
  getCloudAccessToken,
31433
- e2ee
31624
+ e2ee,
31625
+ attachments
31434
31626
  } = opts;
31435
31627
  const preferredForPrompt = agentType ?? backendFallbackAgentType ?? null;
31436
31628
  pendingCancelRunId = void 0;
@@ -31501,7 +31693,8 @@ async function createAcpManager(options) {
31501
31693
  sessionChangeSummaryFilePaths,
31502
31694
  cloudApiBaseUrl,
31503
31695
  getCloudAccessToken,
31504
- e2ee
31696
+ e2ee,
31697
+ attachments
31505
31698
  });
31506
31699
  }
31507
31700
  void run().finally(() => {
@@ -34471,6 +34664,7 @@ function reportGitRepos(getWs, log2) {
34471
34664
  var API_TO_BRIDGE_MESSAGE_TYPES = [
34472
34665
  "auth_token",
34473
34666
  "bridge_identified",
34667
+ "ha",
34474
34668
  "dev_servers_config",
34475
34669
  "server_control",
34476
34670
  "agent_config",
@@ -34499,6 +34693,10 @@ function parseApiToBridgeMessage(data, log2) {
34499
34693
  }
34500
34694
  return null;
34501
34695
  }
34696
+ if (t === "ha") {
34697
+ const s = data.s;
34698
+ if (typeof s !== "number" || !Number.isFinite(s)) return null;
34699
+ }
34502
34700
  return data;
34503
34701
  }
34504
34702
 
@@ -34539,6 +34737,13 @@ var handleBridgeIdentified = (msg, deps) => {
34539
34737
  });
34540
34738
  };
34541
34739
 
34740
+ // src/connection/heartbeat/ack.ts
34741
+ var handleBridgeHeartbeatAck = (msg, deps) => {
34742
+ const raw = msg.s;
34743
+ if (typeof raw !== "number" || !Number.isFinite(raw)) return;
34744
+ deps.onBridgeHeartbeatAck?.(Math.trunc(raw));
34745
+ };
34746
+
34542
34747
  // src/agents/acp/from-bridge/handle-bridge-agent-config.ts
34543
34748
  function handleBridgeAgentConfig(msg, { acpManager }) {
34544
34749
  if (!Array.isArray(msg.agents) || msg.agents.length === 0) return;
@@ -34705,7 +34910,8 @@ function dispatchLocalPrompt(next, deps) {
34705
34910
  ...Array.isArray(pl.sessionChangeSummaryFilePaths) ? { sessionChangeSummaryFilePaths: pl.sessionChangeSummaryFilePaths } : {},
34706
34911
  ...Array.isArray(pl.sessionChangeSummaryFileSnapshots) ? { sessionChangeSummaryFileSnapshots: pl.sessionChangeSummaryFileSnapshots } : {},
34707
34912
  ...typeof pl.agentType === "string" && pl.agentType.trim() ? { agentType: pl.agentType.trim() } : {},
34708
- ...pl.agentConfig != null && typeof pl.agentConfig === "object" && !Array.isArray(pl.agentConfig) && Object.keys(pl.agentConfig).length > 0 ? { agentConfig: pl.agentConfig } : {}
34913
+ ...pl.agentConfig != null && typeof pl.agentConfig === "object" && !Array.isArray(pl.agentConfig) && Object.keys(pl.agentConfig).length > 0 ? { agentConfig: pl.agentConfig } : {},
34914
+ ...Array.isArray(pl.attachments) && pl.attachments.length > 0 ? { attachments: pl.attachments } : {}
34709
34915
  };
34710
34916
  handleBridgePrompt(msg, deps);
34711
34917
  }
@@ -34916,32 +35122,6 @@ function parseFollowUpFieldsFromPromptMessage(msg) {
34916
35122
  return { followUpCatalogPromptId, sessionChangeSummaryFilePaths, sessionChangeSummaryFileSnapshots };
34917
35123
  }
34918
35124
 
34919
- // ../e2ee/src/constants.ts
34920
- var E2EE_NONCE_BYTES = 12;
34921
-
34922
- // ../e2ee/src/types.ts
34923
- function isE2eeEnvelope(value) {
34924
- if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
34925
- const o = value;
34926
- return typeof o.k === "string" && typeof o.n === "string" && typeof o.c === "string";
34927
- }
34928
-
34929
- // ../e2ee/src/encoding.ts
34930
- function base64UrlEncode(bytes) {
34931
- let binary = "";
34932
- for (let i = 0; i < bytes.length; i += 1) binary += String.fromCharCode(bytes[i]);
34933
- const b64 = btoa(binary);
34934
- return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
34935
- }
34936
- function base64UrlDecode(value) {
34937
- const b64 = value.replace(/-/g, "+").replace(/_/g, "/");
34938
- const padded = b64 + "=".repeat((4 - b64.length % 4) % 4);
34939
- const binary = atob(padded);
34940
- const out = new Uint8Array(binary.length);
34941
- for (let i = 0; i < binary.length; i += 1) out[i] = binary.charCodeAt(i);
34942
- return out;
34943
- }
34944
-
34945
35125
  // src/agents/acp/change-summary/decrypt-change-summary-file-input.ts
34946
35126
  function decryptChangeSummaryFileInput(row, e2ee) {
34947
35127
  if (!e2ee) return row;
@@ -34998,15 +35178,30 @@ function resolveChangeSummaryPromptForAgent(params) {
34998
35178
  }
34999
35179
 
35000
35180
  // src/agents/acp/from-bridge/handle-bridge-prompt.ts
35181
+ function parseBridgeAttachments(msg) {
35182
+ const raw = msg.attachments;
35183
+ if (!Array.isArray(raw)) return [];
35184
+ const out = [];
35185
+ for (const x of raw) {
35186
+ if (x === null || typeof x !== "object" || Array.isArray(x)) continue;
35187
+ const o = x;
35188
+ const id = typeof o.attachmentId === "string" ? o.attachmentId.trim() : "";
35189
+ if (!id) continue;
35190
+ const mt = typeof o.mimeType === "string" && o.mimeType.trim() ? o.mimeType.trim() : "application/octet-stream";
35191
+ out.push({ attachmentId: id, mimeType: mt });
35192
+ }
35193
+ return out;
35194
+ }
35001
35195
  function handleBridgePrompt(msg, deps) {
35002
35196
  const { getWs, log: log2, acpManager, sessionWorktreeManager } = deps;
35003
35197
  const rawPrompt = msg.prompt;
35004
35198
  const promptText = typeof rawPrompt === "string" ? rawPrompt : rawPrompt != null ? String(rawPrompt) : "";
35199
+ const attachments = parseBridgeAttachments(msg);
35005
35200
  const sessionId = msg.sessionId;
35006
35201
  const runId = typeof msg.runId === "string" ? msg.runId : void 0;
35007
35202
  const promptId = typeof msg.id === "string" ? msg.id : void 0;
35008
35203
  const { sendBridgeMessage, sendResult: sendResult2, sendSessionUpdate } = createBridgePromptSenders(deps, getWs);
35009
- if (!promptText.trim()) {
35204
+ if (!promptText.trim() && attachments.length === 0) {
35010
35205
  log2(
35011
35206
  `[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).`
35012
35207
  );
@@ -35073,7 +35268,8 @@ function handleBridgePrompt(msg, deps) {
35073
35268
  sessionChangeSummaryFilePaths: pathsForUpload,
35074
35269
  cloudApiBaseUrl: deps.cloudApiBaseUrl,
35075
35270
  getCloudAccessToken: deps.getCloudAccessToken,
35076
- e2ee: deps.e2ee
35271
+ e2ee: deps.e2ee,
35272
+ ...attachments.length > 0 ? { attachments } : {}
35077
35273
  });
35078
35274
  }
35079
35275
  void sessionWorktreeManager.resolveSessionParentPathForPrompt(sessionId, { isNewSession, sessionParent, sessionParentPath }).then((cwd) => preambleAndPrompt(cwd)).catch((err) => {
@@ -35717,6 +35913,9 @@ function dispatchBridgeMessage(msg, deps) {
35717
35913
  case "bridge_identified":
35718
35914
  handleBridgeIdentified(msg, deps);
35719
35915
  break;
35916
+ case "ha":
35917
+ handleBridgeHeartbeatAck(msg, deps);
35918
+ break;
35720
35919
  case "dev_servers_config":
35721
35920
  handleDevServersConfig(msg, deps);
35722
35921
  break;
@@ -35772,9 +35971,17 @@ function dispatchBridgeMessage(msg, deps) {
35772
35971
  }
35773
35972
 
35774
35973
  // src/routing/handle-bridge-message.ts
35974
+ function normalizeInboundBridgeWebSocketJson(data) {
35975
+ if (data === null || typeof data !== "object" || Array.isArray(data)) return data;
35976
+ const o = data;
35977
+ if (o.t === "ha" && typeof o.s === "number" && Number.isFinite(o.s)) {
35978
+ return { type: "ha", s: Math.trunc(o.s) };
35979
+ }
35980
+ return data;
35981
+ }
35775
35982
  function handleBridgeMessage(data, deps) {
35776
35983
  if (!deps.getWs()) return;
35777
- const msg = parseApiToBridgeMessage(data, deps.log);
35984
+ const msg = parseApiToBridgeMessage(normalizeInboundBridgeWebSocketJson(data), deps.log);
35778
35985
  if (!msg) return;
35779
35986
  setImmediate(() => {
35780
35987
  dispatchBridgeMessage(msg, deps);
@@ -35822,7 +36029,8 @@ function createMainBridgeWebSocketLifecycle(params) {
35822
36029
  persistTokens,
35823
36030
  onAuthInvalid,
35824
36031
  e2ee,
35825
- identifyReportedPaths
36032
+ identifyReportedPaths,
36033
+ bridgeHeartbeat
35826
36034
  } = params;
35827
36035
  let authRefreshInFlight = false;
35828
36036
  function handleOpen() {
@@ -35854,6 +36062,7 @@ function createMainBridgeWebSocketLifecycle(params) {
35854
36062
  }
35855
36063
  }
35856
36064
  function handleClose(code, reason) {
36065
+ bridgeHeartbeat?.stop();
35857
36066
  try {
35858
36067
  const was = state.currentWs;
35859
36068
  state.currentWs = null;
@@ -35888,6 +36097,7 @@ function createMainBridgeWebSocketLifecycle(params) {
35888
36097
  } catch {
35889
36098
  }
35890
36099
  }
36100
+ bridgeHeartbeat?.stop();
35891
36101
  const prev = state.currentWs;
35892
36102
  if (prev) {
35893
36103
  prev.removeAllListeners();
@@ -36043,6 +36253,103 @@ function createCliE2eeRuntime(key) {
36043
36253
  const merged = { ...message, ...parsed };
36044
36254
  delete merged.ee;
36045
36255
  return merged;
36256
+ },
36257
+ decryptEnvelopeToBuffer(envelope) {
36258
+ if (envelope.k !== key.id) throw new Error(`E2EE key mismatch: ${envelope.k}`);
36259
+ const sealed = Buffer.from(base64UrlDecode(envelope.c));
36260
+ if (sealed.length < 16) throw new Error("Invalid E2EE payload.");
36261
+ const ciphertext = sealed.subarray(0, sealed.length - 16);
36262
+ const tag = sealed.subarray(sealed.length - 16);
36263
+ const nonce = Buffer.from(base64UrlDecode(envelope.n));
36264
+ const decipher = createDecipheriv("aes-256-gcm", rawKey, nonce);
36265
+ decipher.setAuthTag(tag);
36266
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
36267
+ }
36268
+ };
36269
+ }
36270
+
36271
+ // src/connection/heartbeat/controller.ts
36272
+ function meanRttMs(samples) {
36273
+ if (samples.length === 0) return void 0;
36274
+ let sum = 0;
36275
+ for (const x of samples) sum += x;
36276
+ return sum / samples.length;
36277
+ }
36278
+ function createBridgeHeartbeatController(params) {
36279
+ const { getWs, log: log2 } = params;
36280
+ let interval = null;
36281
+ let seqCursor = -1;
36282
+ let awaitingSeq = null;
36283
+ let sentAtMs = 0;
36284
+ let missed = 0;
36285
+ const rttSamples = [];
36286
+ function clearTimer() {
36287
+ if (interval != null) {
36288
+ clearInterval(interval);
36289
+ interval = null;
36290
+ }
36291
+ }
36292
+ function nextSeq() {
36293
+ seqCursor = seqCursor >= BRIDGE_HEARTBEAT_SEQ_MAX ? 0 : seqCursor + 1;
36294
+ return seqCursor;
36295
+ }
36296
+ function tick() {
36297
+ const ws = getWs();
36298
+ if (!ws || ws.readyState !== wrapper_default.OPEN) return;
36299
+ if (awaitingSeq !== null) {
36300
+ missed++;
36301
+ if (missed >= BRIDGE_HEARTBEAT_MISSED_ACKS_BEFORE_RECONNECT) {
36302
+ try {
36303
+ log2("[Bridge service] Heartbeat missed repeatedly; reconnecting\u2026");
36304
+ } catch {
36305
+ }
36306
+ clearTimer();
36307
+ awaitingSeq = null;
36308
+ missed = 0;
36309
+ rttSamples.length = 0;
36310
+ safeCloseWebSocket(ws);
36311
+ return;
36312
+ }
36313
+ }
36314
+ const seq = nextSeq();
36315
+ const mean = meanRttMs(rttSamples);
36316
+ const payload = mean !== void 0 && Number.isFinite(mean) ? { t: "h", s: seq, m: Math.round(mean) } : { t: "h", s: seq };
36317
+ sendWsMessage(ws, payload);
36318
+ awaitingSeq = seq;
36319
+ sentAtMs = Date.now();
36320
+ }
36321
+ return {
36322
+ start() {
36323
+ clearTimer();
36324
+ awaitingSeq = null;
36325
+ missed = 0;
36326
+ seqCursor = -1;
36327
+ rttSamples.length = 0;
36328
+ interval = setInterval(tick, BRIDGE_APP_HEARTBEAT_INTERVAL_MS);
36329
+ },
36330
+ stop() {
36331
+ clearTimer();
36332
+ awaitingSeq = null;
36333
+ missed = 0;
36334
+ rttSamples.length = 0;
36335
+ },
36336
+ onAck(seq) {
36337
+ if (awaitingSeq === null) return;
36338
+ if (!Number.isFinite(seq)) return;
36339
+ const ack = Math.trunc(seq);
36340
+ const pending = awaitingSeq;
36341
+ if (ack < pending) return;
36342
+ if (ack === pending) {
36343
+ const rtt = Date.now() - sentAtMs;
36344
+ if (Number.isFinite(rtt) && rtt >= 0 && rtt < 6e5) {
36345
+ rttSamples.push(rtt);
36346
+ if (rttSamples.length > BRIDGE_HEARTBEAT_RTT_SAMPLE_MAX) {
36347
+ rttSamples.splice(0, rttSamples.length - BRIDGE_HEARTBEAT_RTT_SAMPLE_MAX);
36348
+ }
36349
+ }
36350
+ }
36351
+ awaitingSeq = null;
36352
+ missed = 0;
36046
36353
  }
36047
36354
  };
36048
36355
  }
@@ -36086,13 +36393,18 @@ async function createBridgeConnection(options) {
36086
36393
  }
36087
36394
  const e2ee = options.e2eCertificate ? createCliE2eeRuntime(options.e2eCertificate) : void 0;
36088
36395
  const devServerManager = new DevServerManager({ getWs, log: logFn, getBridgeRoot, e2ee });
36089
- const onBridgeIdentified = createOnBridgeIdentified({
36396
+ const bridgeHeartbeat = createBridgeHeartbeatController({ getWs, log: logFn });
36397
+ const baseOnBridgeIdentified = createOnBridgeIdentified({
36090
36398
  devServerManager,
36091
36399
  firehoseServerUrl,
36092
36400
  workspaceId,
36093
36401
  state,
36094
36402
  logFn
36095
36403
  });
36404
+ const onBridgeIdentified = (msg) => {
36405
+ baseOnBridgeIdentified(msg);
36406
+ bridgeHeartbeat.start();
36407
+ };
36096
36408
  const sendLocalSkillsReport = createSendLocalSkillsReport(getWs, logFn);
36097
36409
  const reportAutoDetectedAgents = createReportAutoDetectedAgents(getWs, logFn);
36098
36410
  const messageDeps = {
@@ -36101,6 +36413,9 @@ async function createBridgeConnection(options) {
36101
36413
  acpManager,
36102
36414
  sessionWorktreeManager,
36103
36415
  onBridgeIdentified,
36416
+ onBridgeHeartbeatAck: (seq) => {
36417
+ bridgeHeartbeat.onAck(seq);
36418
+ },
36104
36419
  sendLocalSkillsReport,
36105
36420
  reportAutoDetectedAgents,
36106
36421
  devServerManager,
@@ -36124,13 +36439,15 @@ async function createBridgeConnection(options) {
36124
36439
  persistTokens,
36125
36440
  onAuthInvalid,
36126
36441
  e2ee,
36127
- identifyReportedPaths
36442
+ identifyReportedPaths,
36443
+ bridgeHeartbeat
36128
36444
  });
36129
36445
  connect();
36130
36446
  const stopFileIndexWatcher = startFileIndexWatcher(getBridgeRoot());
36131
36447
  return {
36132
36448
  close: async () => {
36133
36449
  stopFileIndexWatcher();
36450
+ bridgeHeartbeat.stop();
36134
36451
  await closeBridgeConnection(state, acpManager, devServerManager, logFn);
36135
36452
  }
36136
36453
  };