happy-imou-cloud 2.1.10 → 2.1.12

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 (32) hide show
  1. package/bin/happy-cloud.mjs +1 -1
  2. package/dist/{BaseReasoningProcessor-DbV-vhFy.mjs → BaseReasoningProcessor-DgKkEyg_.mjs} +2 -2
  3. package/dist/{BaseReasoningProcessor-8GjgD9Nk.cjs → BaseReasoningProcessor-yYne8hbQ.cjs} +2 -2
  4. package/dist/{ProviderSelectionHandler-DWeq9rG2.cjs → ProviderSelectionHandler-CARO_dnJ.cjs} +2 -2
  5. package/dist/{ProviderSelectionHandler-DXgu2zND.mjs → ProviderSelectionHandler-CxLijFCe.mjs} +2 -2
  6. package/dist/{api-Dp5_DJo0.mjs → api-0mR8QJK2.mjs} +266 -29
  7. package/dist/{api-8p1d7M3Z.cjs → api-CwRFeZQw.cjs} +266 -29
  8. package/dist/{command-BtjeL-Z7.cjs → command-0ypdkfyo.cjs} +3 -3
  9. package/dist/{command-DRtgCMGi.mjs → command-Dh_oQxDu.mjs} +3 -3
  10. package/dist/{index-Dz1Zo1-d.cjs → index-IkNv7MfY.cjs} +114 -17
  11. package/dist/{index-C_MTvw5p.mjs → index-zIZBoE62.mjs} +112 -15
  12. package/dist/index.cjs +3 -3
  13. package/dist/index.mjs +3 -3
  14. package/dist/lib.cjs +1 -1
  15. package/dist/lib.d.cts +282 -17
  16. package/dist/lib.d.mts +282 -17
  17. package/dist/lib.mjs +1 -1
  18. package/dist/{persistence-De7JoaQp.cjs → persistence-BlqXzujW.cjs} +1 -1
  19. package/dist/{persistence-Ds1cKgQ_.mjs → persistence-CZEdRTGx.mjs} +1 -1
  20. package/dist/{registerKillSessionHandler-DL07DW1o.cjs → registerKillSessionHandler-B0an4vUf.cjs} +133 -9
  21. package/dist/{registerKillSessionHandler-Bqd1W5ZT.mjs → registerKillSessionHandler-DYg4cQCz.mjs} +133 -9
  22. package/dist/{runClaude-DF_hjle_.cjs → runClaude-C6PA0-0n.cjs} +15 -7
  23. package/dist/{runClaude-BtMjqmaI.mjs → runClaude-DWeTS-VL.mjs} +15 -7
  24. package/dist/{runCodex-MtvDLX7s.cjs → runCodex-B_TM-cqA.cjs} +15 -8
  25. package/dist/{runCodex-CxtZTXtC.mjs → runCodex-DUXczsJP.mjs} +15 -8
  26. package/dist/{runGemini-Bf8tmYeU.cjs → runGemini-0-mnECiy.cjs} +15 -7
  27. package/dist/{runGemini-CmRPi60L.mjs → runGemini-CBpN6a9w.mjs} +15 -7
  28. package/package.json +1 -1
  29. package/scripts/devtools/README.md +9 -9
  30. package/scripts/e2e/fake-codex-acp-agent.mjs +139 -139
  31. package/scripts/e2e/local-server-session-roundtrip.mjs +1063 -1063
  32. package/scripts/ensureAcpSdkCompat.mjs +1 -1
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  import { execFileSync } from 'child_process';
4
4
  import { fileURLToPath } from 'url';
@@ -1,5 +1,5 @@
1
- import { a as createSessionMetadata, p as publishSessionRegistration } from './index-C_MTvw5p.mjs';
2
- import { s as startOfflineReconnection, c as configuration, i as isAuthenticationRequiredError, l as logger } from './api-Dp5_DJo0.mjs';
1
+ import { a as createSessionMetadata, p as publishSessionRegistration } from './index-zIZBoE62.mjs';
2
+ import { s as startOfflineReconnection, c as configuration, i as isAuthenticationRequiredError, l as logger } from './api-0mR8QJK2.mjs';
3
3
  import { EventEmitter } from 'node:events';
4
4
  import { randomUUID } from 'node:crypto';
5
5
 
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
- var index = require('./index-Dz1Zo1-d.cjs');
4
- var api = require('./api-8p1d7M3Z.cjs');
3
+ var index = require('./index-IkNv7MfY.cjs');
4
+ var api = require('./api-CwRFeZQw.cjs');
5
5
  var node_events = require('node:events');
6
6
  var node_crypto = require('node:crypto');
7
7
 
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
- var api = require('./api-8p1d7M3Z.cjs');
4
- var registerKillSessionHandler = require('./registerKillSessionHandler-DL07DW1o.cjs');
3
+ var api = require('./api-CwRFeZQw.cjs');
4
+ var registerKillSessionHandler = require('./registerKillSessionHandler-B0an4vUf.cjs');
5
5
 
6
6
  async function runModeLoop(opts) {
7
7
  let currentMode = opts.startingMode;
@@ -1,5 +1,5 @@
1
- import { l as logger } from './api-Dp5_DJo0.mjs';
2
- import { g as getPendingInteractionTimeoutMs, I as INTERACTION_SUPERSEDED_ERROR, a as INTERACTION_TIMED_OUT_ERROR } from './registerKillSessionHandler-Bqd1W5ZT.mjs';
1
+ import { l as logger } from './api-0mR8QJK2.mjs';
2
+ import { g as getPendingInteractionTimeoutMs, I as INTERACTION_SUPERSEDED_ERROR, a as INTERACTION_TIMED_OUT_ERROR } from './registerKillSessionHandler-DYg4cQCz.mjs';
3
3
 
4
4
  async function runModeLoop(opts) {
5
5
  let currentMode = opts.startingMode;
@@ -16,7 +16,7 @@ import { spawn } from 'node:child_process';
16
16
  import { Expo } from 'expo-server-sdk';
17
17
 
18
18
  var name = "happy-imou-cloud";
19
- var version = "2.1.10";
19
+ var version = "2.1.12";
20
20
  var description = "hicloud - Imou 企业定制版。关键是 happy!移动端远程 AI 编程工具,支持 Claude Code、Codex 和 Gemini CLI";
21
21
  var author = "long.zhu";
22
22
  var license = "MIT";
@@ -431,7 +431,7 @@ async function listDaemonLogFiles(limit = 50) {
431
431
  return { file, path: fullPath, modified: stats.mtime };
432
432
  }).sort((a, b) => b.modified.getTime() - a.modified.getTime());
433
433
  try {
434
- const { readDaemonState } = await import('./persistence-Ds1cKgQ_.mjs');
434
+ const { readDaemonState } = await import('./persistence-CZEdRTGx.mjs');
435
435
  const state = await readDaemonState();
436
436
  if (!state) {
437
437
  return logs;
@@ -592,9 +592,23 @@ const HappyOrgTaskControlSchema = z.object({
592
592
  newDecision: z.string().min(1).optional().nullable(),
593
593
  newResource: z.string().min(1).optional().nullable()
594
594
  });
595
+ const HappyOrgReplyContextSchema = z.object({
596
+ dispatchId: z.string().min(1),
597
+ scope: z.string().min(1),
598
+ replyTo: z.string().min(1)
599
+ });
600
+ const HappyOrgSpecialistHomeIdentitySchema = z.object({
601
+ homeSlug: z.string().min(1),
602
+ path: z.string().min(1).nullish(),
603
+ happySessionId: z.string().min(1).nullish(),
604
+ machineId: z.string().min(1).nullish(),
605
+ startedBy: z.enum(["daemon", "terminal"]).nullish(),
606
+ flavor: z.string().min(1).nullish()
607
+ });
595
608
  const HappyOrgMessageMetaSchema = z.object({
596
609
  taskContext: HappyOrgTaskContextSchema.optional(),
597
- control: HappyOrgTaskControlSchema.optional()
610
+ control: HappyOrgTaskControlSchema.optional(),
611
+ replyContext: HappyOrgReplyContextSchema.optional()
598
612
  });
599
613
  z.object({
600
614
  turnStatus: HappyOrgTurnStatusSchema.optional().nullable(),
@@ -611,7 +625,9 @@ const HappyOrgTurnReportSchema = HappyOrgTaskContextSchema.extend({
611
625
  blockerCode: z.string().nullable(),
612
626
  decisionNeeded: z.string().nullable(),
613
627
  targetArtifact: z.string().nullable(),
614
- repeatFingerprint: z.string().nullable()
628
+ repeatFingerprint: z.string().nullable(),
629
+ replyContext: HappyOrgReplyContextSchema.nullish(),
630
+ specialistHome: HappyOrgSpecialistHomeIdentitySchema.nullish()
615
631
  });
616
632
  const HappyOrgRepeatEntrySchema = z.object({
617
633
  count: z.number().int().nonnegative(),
@@ -638,6 +654,8 @@ z.object({
638
654
  taskContext: HappyOrgTaskContextSchema.optional(),
639
655
  runtime: HappyOrgRuntimeStateSchema.optional(),
640
656
  activeOwner: HappyOrgTaskOwnershipSchema.nullish(),
657
+ replyContext: HappyOrgReplyContextSchema.nullish(),
658
+ specialistHome: HappyOrgSpecialistHomeIdentitySchema.nullish(),
641
659
  repeat: z.object({
642
660
  threshold: z.number().int().positive(),
643
661
  fingerprints: z.record(z.string(), HappyOrgRepeatEntrySchema)
@@ -1737,6 +1755,8 @@ const MAX_PENDING_RELIABLE_SESSION_MESSAGE_BYTES = 512 * 1024;
1737
1755
  const PROTOCOL_V3_INITIAL_SNAPSHOT_LIMIT = 150;
1738
1756
  const PROTOCOL_V3_CHANGES_PAGE_LIMIT = 200;
1739
1757
  const PROTOCOL_V3_CAPABILITIES_WAIT_MS = 250;
1758
+ const COMMITTED_SESSION_WRITE_ACK_TIMEOUT_MS = 5e3;
1759
+ const RELIABLE_SESSION_MESSAGE_RETRY_DELAY_MS = 250;
1740
1760
  class ApiSessionClient extends EventEmitter {
1741
1761
  credentials;
1742
1762
  sessionId;
@@ -1752,8 +1772,12 @@ class ApiSessionClient extends EventEmitter {
1752
1772
  metadataLock = new AsyncLock();
1753
1773
  encryptionKey;
1754
1774
  encryptionVariant;
1755
- pendingReliableSessionMessages = [];
1756
- pendingReliableSessionMessageBytes = 0;
1775
+ queuedReliableSessionMessages = [];
1776
+ queuedReliableSessionMessageBytes = 0;
1777
+ inFlightReliableSessionMessage = null;
1778
+ reliableSessionMessageFlushInProgress = false;
1779
+ reliableSessionMessageDrainWaiters = [];
1780
+ reliableSessionMessageRetryTimer = null;
1757
1781
  reconnectAfterServerDisconnectTimer = null;
1758
1782
  lastSocketServerError = null;
1759
1783
  protocolV3SessionSync = null;
@@ -1765,6 +1789,7 @@ class ApiSessionClient extends EventEmitter {
1765
1789
  protocolV3Descriptor = null;
1766
1790
  protocolV3SocketCapabilities = null;
1767
1791
  protocolV3SessionSyncWaitTimer = null;
1792
+ committedSessionWriteAckMode;
1768
1793
  constructor(credentials, session) {
1769
1794
  super();
1770
1795
  this.credentials = credentials;
@@ -1776,6 +1801,7 @@ class ApiSessionClient extends EventEmitter {
1776
1801
  this.encryptionKey = session.encryptionKey;
1777
1802
  this.encryptionVariant = session.encryptionVariant;
1778
1803
  this.lastChangeSeq = session.seq;
1804
+ this.committedSessionWriteAckMode = this.credentials.signing ? "unknown" : "unsupported";
1779
1805
  this.rpcHandlerManager = new RpcHandlerManager({
1780
1806
  scopePrefix: this.sessionId,
1781
1807
  encryptionKey: this.encryptionKey,
@@ -1808,11 +1834,13 @@ class ApiSessionClient extends EventEmitter {
1808
1834
  this.socket.on("connect", () => {
1809
1835
  logger.debug("Socket connected successfully");
1810
1836
  this.clearReconnectAfterServerDisconnectTimer();
1837
+ this.clearReliableSessionMessageRetryTimer();
1811
1838
  this.lastSocketServerError = null;
1812
1839
  this.rpcHandlerManager.onSocketConnect(this.socket);
1813
- this.flushReliableSessionMessages();
1814
1840
  this.protocolV3Descriptor = null;
1815
1841
  this.protocolV3SocketCapabilities = null;
1842
+ this.committedSessionWriteAckMode = this.credentials.signing ? "unknown" : "unsupported";
1843
+ this.flushReliableSessionMessages();
1816
1844
  this.scheduleProtocolV3SessionSync();
1817
1845
  });
1818
1846
  this.socket.on("rpc-request", async (data, callback) => {
@@ -1820,8 +1848,10 @@ class ApiSessionClient extends EventEmitter {
1820
1848
  });
1821
1849
  this.socket.on("disconnect", (reason) => {
1822
1850
  logger.debug("[API] Socket disconnected:", reason);
1851
+ this.clearReliableSessionMessageRetryTimer();
1823
1852
  this.rpcHandlerManager.onSocketDisconnect();
1824
1853
  this.invalidateProtocolV3SessionSync();
1854
+ this.resolveReliableSessionMessageDrainWaitersIfIdle();
1825
1855
  this.retryAfterServerDisconnect(reason);
1826
1856
  });
1827
1857
  this.socket.on("connect_error", (error) => {
@@ -1849,8 +1879,10 @@ class ApiSessionClient extends EventEmitter {
1849
1879
  connectionType: data.connectionType,
1850
1880
  capabilities: data.protocol.capabilities
1851
1881
  });
1882
+ this.committedSessionWriteAckMode = this.supportsKnownProtocolCapability("message_ack_v3") ? "supported" : "unsupported";
1852
1883
  this.clearProtocolV3SessionSyncWaitTimer();
1853
1884
  this.scheduleProtocolV3SessionSync({ waitForCapabilities: false });
1885
+ this.flushReliableSessionMessages();
1854
1886
  } catch (error) {
1855
1887
  logger.debug("[SOCKET] [CAPABILITIES] [ERROR] Error handling capabilities event", { error });
1856
1888
  }
@@ -1990,15 +2022,24 @@ class ApiSessionClient extends EventEmitter {
1990
2022
  };
1991
2023
  const encrypted = encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, content));
1992
2024
  const eventType = typeof body?.type === "string" ? body.type : "unknown";
2025
+ const reliableMessage = {
2026
+ encrypted,
2027
+ type: `codex:${eventType}`,
2028
+ localId: randomUUID()
2029
+ };
1993
2030
  if (!this.socket.connected) {
1994
2031
  if (this.shouldBufferReliableCodexMessage(body)) {
1995
2032
  logger.debug("[API] Socket not connected, buffering reliable Codex message:", { type: eventType });
1996
- this.bufferReliableSessionMessage({ encrypted, type: `codex:${eventType}` });
2033
+ this.enqueueReliableSessionMessage(reliableMessage);
1997
2034
  } else {
1998
2035
  logger.debug("[API] Socket not connected, dropping non-critical Codex message:", { type: eventType });
1999
2036
  }
2000
2037
  return;
2001
2038
  }
2039
+ if (this.shouldBufferReliableCodexMessage(body) && this.shouldUseCommittedSessionWriteAcks()) {
2040
+ this.enqueueReliableSessionMessage(reliableMessage);
2041
+ return;
2042
+ }
2002
2043
  this.emitEncryptedSessionMessage(encrypted);
2003
2044
  }
2004
2045
  /**
@@ -2023,13 +2064,18 @@ class ApiSessionClient extends EventEmitter {
2023
2064
  logger.debug(`[SOCKET] Sending ACP message from ${provider}:`, { type: body.type, hasMessage: "message" in body });
2024
2065
  const encrypted = encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, content));
2025
2066
  const eventType = typeof body?.type === "string" ? body.type : "unknown";
2067
+ const reliableMessage = {
2068
+ encrypted,
2069
+ type: `acp:${provider}:${eventType}`,
2070
+ localId: randomUUID()
2071
+ };
2026
2072
  if (!this.socket.connected) {
2027
2073
  if (this.shouldBufferReliableAcpMessage(body)) {
2028
2074
  logger.debug("[API] Socket not connected, buffering reliable ACP message:", {
2029
2075
  provider,
2030
2076
  type: eventType
2031
2077
  });
2032
- this.bufferReliableSessionMessage({ encrypted, type: `acp:${provider}:${eventType}` });
2078
+ this.enqueueReliableSessionMessage(reliableMessage);
2033
2079
  } else {
2034
2080
  logger.debug("[API] Socket not connected, dropping non-critical ACP message:", {
2035
2081
  provider,
@@ -2038,10 +2084,14 @@ class ApiSessionClient extends EventEmitter {
2038
2084
  }
2039
2085
  return;
2040
2086
  }
2087
+ if (this.shouldBufferReliableAcpMessage(body) && this.shouldUseCommittedSessionWriteAcks()) {
2088
+ this.enqueueReliableSessionMessage(reliableMessage);
2089
+ return;
2090
+ }
2041
2091
  this.emitEncryptedSessionMessage(encrypted);
2042
2092
  }
2043
2093
  sendSessionEvent(event, id) {
2044
- let content = {
2094
+ const content = {
2045
2095
  role: "agent",
2046
2096
  content: {
2047
2097
  id: id ?? randomUUID(),
@@ -2050,15 +2100,24 @@ class ApiSessionClient extends EventEmitter {
2050
2100
  }
2051
2101
  };
2052
2102
  const encrypted = encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, content));
2103
+ const reliableMessage = {
2104
+ encrypted,
2105
+ type: `event:${event.type}`,
2106
+ localId: content.content.id
2107
+ };
2053
2108
  if (!this.socket.connected) {
2054
2109
  if (this.shouldBufferReliableSessionEvent(event)) {
2055
2110
  logger.debug("[API] Socket not connected, buffering session event:", { type: event.type });
2056
- this.bufferReliableSessionMessage({ encrypted, type: `event:${event.type}` });
2111
+ this.enqueueReliableSessionMessage(reliableMessage);
2057
2112
  } else {
2058
2113
  logger.debug("[API] Socket not connected, dropping session event:", { type: event.type });
2059
2114
  }
2060
2115
  return;
2061
2116
  }
2117
+ if (this.shouldBufferReliableSessionEvent(event) && this.shouldUseCommittedSessionWriteAcks()) {
2118
+ this.enqueueReliableSessionMessage(reliableMessage);
2119
+ return;
2120
+ }
2062
2121
  this.emitEncryptedSessionMessage(encrypted);
2063
2122
  }
2064
2123
  /**
@@ -2172,6 +2231,10 @@ class ApiSessionClient extends EventEmitter {
2172
2231
  * Wait for socket buffer to flush
2173
2232
  */
2174
2233
  async flush() {
2234
+ if (!this.socket.connected) {
2235
+ return;
2236
+ }
2237
+ await this.waitForReliableSessionMessagesToDrain();
2175
2238
  if (!this.socket.connected) {
2176
2239
  return;
2177
2240
  }
@@ -2461,31 +2524,197 @@ class ApiSessionClient extends EventEmitter {
2461
2524
  supportsKnownProtocolCapability(capability) {
2462
2525
  return Array.isArray(this.protocolV3Descriptor?.capabilities) && this.protocolV3Descriptor.capabilities.includes(capability);
2463
2526
  }
2464
- emitEncryptedSessionMessage(encrypted) {
2527
+ shouldUseCommittedSessionWriteAcks() {
2528
+ if (!this.credentials.signing) {
2529
+ return false;
2530
+ }
2531
+ return this.committedSessionWriteAckMode !== "unsupported";
2532
+ }
2533
+ emitEncryptedSessionMessage(encrypted, localId) {
2465
2534
  this.socket.emit("message", {
2466
2535
  sid: this.sessionId,
2467
- message: encrypted
2536
+ message: encrypted,
2537
+ ...typeof localId === "string" ? { localId } : {}
2468
2538
  });
2469
2539
  }
2470
- bufferReliableSessionMessage(message) {
2471
- this.pendingReliableSessionMessages.push(message);
2472
- this.pendingReliableSessionMessageBytes += message.encrypted.length;
2540
+ enqueueReliableSessionMessage(message) {
2541
+ this.queuedReliableSessionMessages.push(message);
2542
+ this.queuedReliableSessionMessageBytes += message.encrypted.length;
2473
2543
  this.trimPendingReliableSessionMessages();
2544
+ this.flushReliableSessionMessages();
2545
+ }
2546
+ dequeueReliableSessionMessage() {
2547
+ const message = this.queuedReliableSessionMessages.shift() ?? null;
2548
+ if (!message) {
2549
+ return null;
2550
+ }
2551
+ this.queuedReliableSessionMessageBytes -= message.encrypted.length;
2552
+ return message;
2474
2553
  }
2475
2554
  flushReliableSessionMessages() {
2476
- if (!this.socket.connected || this.pendingReliableSessionMessages.length === 0) {
2555
+ this.clearReliableSessionMessageRetryTimer();
2556
+ if (this.reliableSessionMessageFlushInProgress || !this.socket.connected) {
2557
+ this.resolveReliableSessionMessageDrainWaitersIfIdle();
2477
2558
  return;
2478
2559
  }
2479
- const buffered = this.pendingReliableSessionMessages.splice(0, this.pendingReliableSessionMessages.length);
2480
- this.pendingReliableSessionMessageBytes = 0;
2481
- logger.debug("[API] Flushing buffered session messages after reconnect", {
2482
- count: buffered.length,
2483
- types: buffered.map((message) => message.type)
2560
+ this.reliableSessionMessageFlushInProgress = true;
2561
+ void (async () => {
2562
+ try {
2563
+ while (this.socket.connected) {
2564
+ const message = this.inFlightReliableSessionMessage ?? this.dequeueReliableSessionMessage();
2565
+ if (!message) {
2566
+ this.inFlightReliableSessionMessage = null;
2567
+ break;
2568
+ }
2569
+ if (this.inFlightReliableSessionMessage === null) {
2570
+ logger.debug("[API] Flushing buffered session message after reconnect", {
2571
+ type: message.type,
2572
+ localId: message.localId
2573
+ });
2574
+ }
2575
+ this.inFlightReliableSessionMessage = message;
2576
+ if (this.shouldUseCommittedSessionWriteAcks()) {
2577
+ const result = await this.emitEncryptedSessionMessageWithAck(message);
2578
+ if (result === "retry") {
2579
+ this.scheduleReliableSessionMessageRetry();
2580
+ break;
2581
+ }
2582
+ } else {
2583
+ this.emitEncryptedSessionMessage(message.encrypted, message.localId);
2584
+ }
2585
+ this.inFlightReliableSessionMessage = null;
2586
+ }
2587
+ } finally {
2588
+ this.reliableSessionMessageFlushInProgress = false;
2589
+ this.resolveReliableSessionMessageDrainWaitersIfIdle();
2590
+ }
2591
+ })();
2592
+ }
2593
+ async waitForReliableSessionMessagesToDrain() {
2594
+ if (!this.socket.connected) {
2595
+ return;
2596
+ }
2597
+ this.flushReliableSessionMessages();
2598
+ if (!this.reliableSessionMessageFlushInProgress && this.inFlightReliableSessionMessage === null && this.queuedReliableSessionMessages.length === 0) {
2599
+ return;
2600
+ }
2601
+ await new Promise((resolve) => {
2602
+ this.reliableSessionMessageDrainWaiters.push(resolve);
2484
2603
  });
2485
- for (const message of buffered) {
2486
- this.emitEncryptedSessionMessage(message.encrypted);
2604
+ }
2605
+ resolveReliableSessionMessageDrainWaitersIfIdle() {
2606
+ if (!this.socket.connected) {
2607
+ const disconnectedWaiters = this.reliableSessionMessageDrainWaiters.splice(0, this.reliableSessionMessageDrainWaiters.length);
2608
+ for (const waiter of disconnectedWaiters) {
2609
+ waiter();
2610
+ }
2611
+ return;
2612
+ }
2613
+ if (this.reliableSessionMessageFlushInProgress || this.inFlightReliableSessionMessage !== null || this.queuedReliableSessionMessages.length > 0) {
2614
+ return;
2615
+ }
2616
+ const waiters = this.reliableSessionMessageDrainWaiters.splice(0, this.reliableSessionMessageDrainWaiters.length);
2617
+ for (const waiter of waiters) {
2618
+ waiter();
2619
+ }
2620
+ }
2621
+ scheduleReliableSessionMessageRetry() {
2622
+ if (this.reliableSessionMessageRetryTimer || !this.socket.connected || this.inFlightReliableSessionMessage === null) {
2623
+ return;
2624
+ }
2625
+ this.reliableSessionMessageRetryTimer = setTimeout(() => {
2626
+ this.reliableSessionMessageRetryTimer = null;
2627
+ if (!this.socket.connected || this.inFlightReliableSessionMessage === null) {
2628
+ this.resolveReliableSessionMessageDrainWaitersIfIdle();
2629
+ return;
2630
+ }
2631
+ this.flushReliableSessionMessages();
2632
+ }, RELIABLE_SESSION_MESSAGE_RETRY_DELAY_MS);
2633
+ }
2634
+ clearReliableSessionMessageRetryTimer() {
2635
+ if (!this.reliableSessionMessageRetryTimer) {
2636
+ return;
2637
+ }
2638
+ clearTimeout(this.reliableSessionMessageRetryTimer);
2639
+ this.reliableSessionMessageRetryTimer = null;
2640
+ }
2641
+ async emitEncryptedSessionMessageWithAck(message) {
2642
+ try {
2643
+ const response = await this.withAckTimeout(
2644
+ Promise.resolve(this.socket.emitWithAck("message", {
2645
+ sid: this.sessionId,
2646
+ message: message.encrypted,
2647
+ localId: message.localId
2648
+ })),
2649
+ COMMITTED_SESSION_WRITE_ACK_TIMEOUT_MS
2650
+ );
2651
+ if (!response || typeof response !== "object" || !("ok" in response)) {
2652
+ if (this.committedSessionWriteAckMode === "unknown") {
2653
+ logger.debug("[API] Falling back to fire-and-forget session writes because websocket write acknowledgements are unavailable", {
2654
+ sessionId: this.sessionId,
2655
+ type: message.type
2656
+ });
2657
+ this.committedSessionWriteAckMode = "unsupported";
2658
+ this.emitEncryptedSessionMessage(message.encrypted, message.localId);
2659
+ return "committed";
2660
+ }
2661
+ logger.debug("[API] Session write acknowledgement payload was invalid, will retry on reconnect", {
2662
+ sessionId: this.sessionId,
2663
+ type: message.type,
2664
+ response
2665
+ });
2666
+ return "retry";
2667
+ }
2668
+ const ackResponse = response;
2669
+ if (!ackResponse.ok) {
2670
+ logger.debug("[API] Session write was rejected by the server", {
2671
+ sessionId: this.sessionId,
2672
+ type: message.type,
2673
+ localId: message.localId,
2674
+ error: ackResponse.error
2675
+ });
2676
+ this.emit("message-ack", ackResponse);
2677
+ return "committed";
2678
+ }
2679
+ this.committedSessionWriteAckMode = "supported";
2680
+ this.emit("message-ack", ackResponse);
2681
+ return "committed";
2682
+ } catch (error) {
2683
+ if (this.committedSessionWriteAckMode === "unknown" && isAckTimeoutError(error)) {
2684
+ logger.debug("[API] Websocket write acknowledgements timed out before protocol capabilities were confirmed, falling back to fire-and-forget writes", {
2685
+ sessionId: this.sessionId,
2686
+ type: message.type
2687
+ });
2688
+ this.committedSessionWriteAckMode = "unsupported";
2689
+ this.emitEncryptedSessionMessage(message.encrypted, message.localId);
2690
+ return "committed";
2691
+ }
2692
+ logger.debug("[API] Reliable session write acknowledgement failed, will retry when the socket reconnects", {
2693
+ sessionId: this.sessionId,
2694
+ type: message.type,
2695
+ localId: message.localId,
2696
+ error
2697
+ });
2698
+ return "retry";
2487
2699
  }
2488
2700
  }
2701
+ async withAckTimeout(promise, timeoutMs) {
2702
+ return await new Promise((resolve, reject) => {
2703
+ const timeout = setTimeout(() => {
2704
+ reject(createAckTimeoutError(timeoutMs));
2705
+ }, timeoutMs);
2706
+ promise.then(
2707
+ (value) => {
2708
+ clearTimeout(timeout);
2709
+ resolve(value);
2710
+ },
2711
+ (error) => {
2712
+ clearTimeout(timeout);
2713
+ reject(error);
2714
+ }
2715
+ );
2716
+ });
2717
+ }
2489
2718
  shouldBufferReliableCodexMessage(body) {
2490
2719
  switch (body?.type) {
2491
2720
  case "message":
@@ -2532,19 +2761,19 @@ class ApiSessionClient extends EventEmitter {
2532
2761
  }
2533
2762
  trimPendingReliableSessionMessages() {
2534
2763
  let dropped = 0;
2535
- while (this.pendingReliableSessionMessages.length > MAX_PENDING_RELIABLE_SESSION_MESSAGES || this.pendingReliableSessionMessageBytes > MAX_PENDING_RELIABLE_SESSION_MESSAGE_BYTES) {
2536
- const removed = this.pendingReliableSessionMessages.shift();
2764
+ while (this.queuedReliableSessionMessages.length > MAX_PENDING_RELIABLE_SESSION_MESSAGES || this.queuedReliableSessionMessageBytes > MAX_PENDING_RELIABLE_SESSION_MESSAGE_BYTES) {
2765
+ const removed = this.queuedReliableSessionMessages.shift();
2537
2766
  if (!removed) {
2538
2767
  break;
2539
2768
  }
2540
- this.pendingReliableSessionMessageBytes -= removed.encrypted.length;
2769
+ this.queuedReliableSessionMessageBytes -= removed.encrypted.length;
2541
2770
  dropped += 1;
2542
2771
  }
2543
2772
  if (dropped > 0) {
2544
2773
  logger.debug("[API] Dropped oldest buffered session messages to cap reconnect memory usage", {
2545
2774
  dropped,
2546
- remaining: this.pendingReliableSessionMessages.length,
2547
- bytes: this.pendingReliableSessionMessageBytes
2775
+ remaining: this.queuedReliableSessionMessages.length,
2776
+ bytes: this.queuedReliableSessionMessageBytes
2548
2777
  });
2549
2778
  }
2550
2779
  }
@@ -2601,6 +2830,14 @@ function createAbortError() {
2601
2830
  error.name = "AbortError";
2602
2831
  return error;
2603
2832
  }
2833
+ function createAckTimeoutError(timeoutMs) {
2834
+ const error = new Error(`Timed out waiting for websocket message acknowledgement after ${timeoutMs}ms`);
2835
+ error.name = "AckTimeoutError";
2836
+ return error;
2837
+ }
2838
+ function isAckTimeoutError(error) {
2839
+ return error instanceof Error && error.name === "AckTimeoutError";
2840
+ }
2604
2841
 
2605
2842
  class ApiMachineClient {
2606
2843
  constructor(credentials, machine) {