@harness-gw/sdk 0.5.1 → 0.6.1

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/README.md CHANGED
@@ -63,7 +63,7 @@ You need a running harness. Hermes is the primary one; see
63
63
  docker run --rm \
64
64
  -v "$PWD/config.yaml:/run/harness-gw/config.yaml:ro" \
65
65
  -v "$PWD/hermes-token:/run/secrets/hermes-token:ro" \
66
- ghcr.io/almogbaku/harness-gw:0.5.1 \
66
+ ghcr.io/almogbaku/harness-gw:0.6.1 \
67
67
  config check --config /run/harness-gw/config.yaml
68
68
  ```
69
69
 
@@ -73,7 +73,7 @@ You need a running harness. Hermes is the primary one; see
73
73
  docker run --rm -p 127.0.0.1:4100:4100 \
74
74
  -v "$PWD/config.yaml:/run/harness-gw/config.yaml:ro" \
75
75
  -v "$PWD/hermes-token:/run/secrets/hermes-token:ro" \
76
- ghcr.io/almogbaku/harness-gw:0.5.1 \
76
+ ghcr.io/almogbaku/harness-gw:0.6.1 \
77
77
  serve --config /run/harness-gw/config.yaml
78
78
  ```
79
79
 
@@ -208,7 +208,7 @@ var SessionMessagePartSchema = z.discriminatedUnion("type", [
208
208
  }),
209
209
  z.strictObject({
210
210
  type: z.literal("image"),
211
- image: z.string().min(1).max(25000000),
211
+ image: z.string().min(1).max(35000000),
212
212
  filename: z.string().min(1).max(4096).optional()
213
213
  }),
214
214
  z.strictObject({
@@ -727,8 +727,10 @@ var HGW_JSONRPC_ERRORS2 = {
727
727
  revisionConflict: -31012,
728
728
  temporarilyUnavailable: -31013,
729
729
  uncertainMutation: -31014,
730
- unsupported: -31015
730
+ unsupported: -31015,
731
+ requestTooLarge: -31016
731
732
  };
733
+ var HGW_MAX_FRAME_BYTES2 = 1100000;
732
734
  var IdentifierSchema2 = z2.string().min(1).max(256).refine((value) => [...value].every((character) => {
733
735
  const code = character.charCodeAt(0);
734
736
  return code >= 32 && code !== 127;
@@ -1180,4 +1182,4 @@ var MCP_APP_SANDBOX_CSP2 = [
1180
1182
  "frame-ancestors 'self'"
1181
1183
  ].join("; ");
1182
1184
 
1183
- export { SESSION_CATALOG_MAX_WINDOW2, RuntimeAuthStateSchema2, OperationCapabilitySchema2, RuntimeInfoSchema2, AgentAvatarSchema2, AgentCatalogEntrySchema2, AgentCatalogResponseSchema2, AgentUpdateFields2, writesAgentField2, AgentUpdateRequestSchema2, AgentUpdateResponseSchema2, SessionStatusSchema2, SessionPlatformSchema2, SessionSchema2, newestSessionFirst2, SessionCatalogResponseSchema2, StopReason2, ToolKind2, ToolLocationSchema2, DiffOperation2, DiffChangeSchema2, ToolDiffSchema2, SessionMessageErrorStatusSchema2, SessionMessageSchema2, SessionPlanActivityMessageSchema2, SessionHistoryResponseSchema2, SessionCreateRequestSchema2, SessionCreateResponseSchema2, SessionPatchRequestSchema2, TurnStopResponseSchema2, TurnSteerRequestSchema2, TurnSteerResponseSchema2, SlashCommandSchema2, MAX_SLASH_COMMANDS2, INTERACTION_PROTOCOL2, SessionWorkspaceCapabilitiesResponseSchema2, GuestRuntimeCapabilitiesResponseSchema2, SessionModelsResponseSchema2, SessionModelUpdateRequestSchema2, SessionModelUpdateResponseSchema2, SessionContextResponseSchema2, ArtifactDescriptorSchema2, SessionTodosResponseSchema2, SessionActivityResponseSchema2, SessionAttachmentStageRequestSchema2, SessionAttachmentStageResponseSchema2, SessionAudioResponseSchema2, SessionTranscriptionRequestSchema2, SessionTranscriptionResponseSchema2, SessionSpeechRequestSchema2, ErrorResponseSchema2, ACP_PROTOCOL_VERSION2, HGW_API_PREFIX2, HGW_ACP_PATH2, HGW_ACP_AGENTS_PATH2, hgwAcpAgentPath2, HGW_META_KEY2, HGW_EXTENSION_VERSION2, HGW_AUTH_METHOD_INVITE2, HGW_ATTACHMENT_URI_SCHEME2, HGW_ARTIFACT_URI_SCHEME2, HGW_METHODS2, HGW_STOP_REASONS2, HGW_PERMISSION_KIND_SESSION2, HGW_PLAN_ID2, HGW_JSONRPC_ERRORS2, IdentifierSchema2, HgwExtensionsSchema2, HgwClientCapabilitiesMetaSchema2, HgwInitializeMetaSchema2, HgwLoginMetaSchema2, HgwSessionNewMetaSchema2, HgwSessionListMetaSchema2, HgwSessionSearchMetaSchema2, HgwSessionSearchResponseSchema2, HgwSessionInfoMetaSchema2, HgwSessionResumeMetaSchema2, HGW_REPLAY_BEFORE2, HgwReplayBeforeSchema2, HgwHistoryCursorSchema2, HgwHistoryPageResponseMetaSchema2, HgwHistoryPageTagSchema2, HgwSessionResumeResponseMetaSchema2, HgwPromptMetaSchema2, HgwSessionUpdateRequestSchema2, HgwSessionPartRequestSchema2, HgwSteerRequestSchema2, HgwSteerResponseSchema2, HgwFocusRequestSchema2, HgwAgentsListResponseSchema2, HgwAgentUpdateRequestSchema2, HgwAgentUpdateResponseSchema2, HgwTurnMetaSchema2, HgwNoticeMetaSchema2, HgwMessageMetaSchema2, HgwCostSchema2, HGW_SUBAGENT_STATUSES2, HgwSubagentSchema2, HgwStateMetaSchema2, HgwChunkMetaSchema2, HgwToolCallMetaSchema2, HgwPlanMetaSchema2, HgwUsageMetaSchema2, HgwAvailableCommandsMetaSchema2, HgwPermissionMetaSchema2, HgwQuestionOptionSchema2, HgwQuestionSchema2, HgwElicitationMetaSchema2, HgwArtifactDescriptorSchema2, formatArtifactUri2, parseArtifactUri2, HgwComposerPrefillNotificationSchema2, HgwErrorNotificationSchema2, HGW_REQUEST_TEXT_MAX2, HgwActivityNotificationSchema2, PushCategorySchema2, categoryOf2, PushLocaleSchema2, PRESENCE_IDLE_MS2, PRESENCE_HEARTBEAT_MS2, PRESENCE_GRACE_MS2, PRESENCE_CLOSED_GRACE_MS2, COALESCE_WINDOW_MS2, PUSH_TEXT_LIMITS2, PushMessageSchema2, PushCategoriesSchema2, PushSubscriptionJsonSchema2, PushInfoSchema2, PushRegistrationSchema2, PushUnregistrationSchema2, CallToolResultSchema2, ReadResourceResultSchema2, McpUiCspSchema2, isMcpAppCspDomain2, McpUiPermissionsSchema2, McpAppFilesSchema2, McpAppViewSchema2, viewerFileResult2, McpAppToolCallRequestSchema2, McpAppResourceReadRequestSchema2, MCP_APP_SANDBOX_PATH2, MCP_APP_SANDBOX_CSP2 };
1185
+ export { SESSION_CATALOG_MAX_WINDOW2, RuntimeAuthStateSchema2, OperationCapabilitySchema2, RuntimeInfoSchema2, AgentAvatarSchema2, AgentCatalogEntrySchema2, AgentCatalogResponseSchema2, AgentUpdateFields2, writesAgentField2, AgentUpdateRequestSchema2, AgentUpdateResponseSchema2, SessionStatusSchema2, SessionPlatformSchema2, SessionSchema2, newestSessionFirst2, SessionCatalogResponseSchema2, StopReason2, ToolKind2, ToolLocationSchema2, DiffOperation2, DiffChangeSchema2, ToolDiffSchema2, SessionMessageErrorStatusSchema2, SessionMessageSchema2, SessionPlanActivityMessageSchema2, SessionHistoryResponseSchema2, SessionCreateRequestSchema2, SessionCreateResponseSchema2, SessionPatchRequestSchema2, TurnStopResponseSchema2, TurnSteerRequestSchema2, TurnSteerResponseSchema2, SlashCommandSchema2, MAX_SLASH_COMMANDS2, INTERACTION_PROTOCOL2, SessionWorkspaceCapabilitiesResponseSchema2, GuestRuntimeCapabilitiesResponseSchema2, SessionModelsResponseSchema2, SessionModelUpdateRequestSchema2, SessionModelUpdateResponseSchema2, SessionContextResponseSchema2, ArtifactDescriptorSchema2, SessionTodosResponseSchema2, SessionActivityResponseSchema2, SessionAttachmentStageRequestSchema2, SessionAttachmentStageResponseSchema2, SessionAudioResponseSchema2, SessionTranscriptionRequestSchema2, SessionTranscriptionResponseSchema2, SessionSpeechRequestSchema2, ErrorResponseSchema2, ACP_PROTOCOL_VERSION2, HGW_API_PREFIX2, HGW_ACP_PATH2, HGW_ACP_AGENTS_PATH2, hgwAcpAgentPath2, HGW_META_KEY2, HGW_EXTENSION_VERSION2, HGW_AUTH_METHOD_INVITE2, HGW_ATTACHMENT_URI_SCHEME2, HGW_ARTIFACT_URI_SCHEME2, HGW_METHODS2, HGW_STOP_REASONS2, HGW_PERMISSION_KIND_SESSION2, HGW_PLAN_ID2, HGW_JSONRPC_ERRORS2, HGW_MAX_FRAME_BYTES2, IdentifierSchema2, HgwExtensionsSchema2, HgwClientCapabilitiesMetaSchema2, HgwInitializeMetaSchema2, HgwLoginMetaSchema2, HgwSessionNewMetaSchema2, HgwSessionListMetaSchema2, HgwSessionSearchMetaSchema2, HgwSessionSearchResponseSchema2, HgwSessionInfoMetaSchema2, HgwSessionResumeMetaSchema2, HGW_REPLAY_BEFORE2, HgwReplayBeforeSchema2, HgwHistoryCursorSchema2, HgwHistoryPageResponseMetaSchema2, HgwHistoryPageTagSchema2, HgwSessionResumeResponseMetaSchema2, HgwPromptMetaSchema2, HgwSessionUpdateRequestSchema2, HgwSessionPartRequestSchema2, HgwSteerRequestSchema2, HgwSteerResponseSchema2, HgwFocusRequestSchema2, HgwAgentsListResponseSchema2, HgwAgentUpdateRequestSchema2, HgwAgentUpdateResponseSchema2, HgwTurnMetaSchema2, HgwNoticeMetaSchema2, HgwMessageMetaSchema2, HgwCostSchema2, HGW_SUBAGENT_STATUSES2, HgwSubagentSchema2, HgwStateMetaSchema2, HgwChunkMetaSchema2, HgwToolCallMetaSchema2, HgwPlanMetaSchema2, HgwUsageMetaSchema2, HgwAvailableCommandsMetaSchema2, HgwPermissionMetaSchema2, HgwQuestionOptionSchema2, HgwQuestionSchema2, HgwElicitationMetaSchema2, HgwArtifactDescriptorSchema2, formatArtifactUri2, parseArtifactUri2, HgwComposerPrefillNotificationSchema2, HgwErrorNotificationSchema2, HGW_REQUEST_TEXT_MAX2, HgwActivityNotificationSchema2, PushCategorySchema2, categoryOf2, PushLocaleSchema2, PRESENCE_IDLE_MS2, PRESENCE_HEARTBEAT_MS2, PRESENCE_GRACE_MS2, PRESENCE_CLOSED_GRACE_MS2, COALESCE_WINDOW_MS2, PUSH_TEXT_LIMITS2, PushMessageSchema2, PushCategoriesSchema2, PushSubscriptionJsonSchema2, PushInfoSchema2, PushRegistrationSchema2, PushUnregistrationSchema2, CallToolResultSchema2, ReadResourceResultSchema2, McpUiCspSchema2, isMcpAppCspDomain2, McpUiPermissionsSchema2, McpAppFilesSchema2, McpAppViewSchema2, viewerFileResult2, McpAppToolCallRequestSchema2, McpAppResourceReadRequestSchema2, MCP_APP_SANDBOX_PATH2, MCP_APP_SANDBOX_CSP2 };
@@ -1,6 +1,7 @@
1
1
  import { type SessionInfo } from "@agentclientprotocol/sdk/experimental/v2";
2
2
  import { type HgwSessionInfoMeta } from "../protocol/acp.js";
3
3
  import type { SessionMetadata, SessionStatus, TodoItem, WorkspaceActivityEvent } from "./workspace.js";
4
+ import type { Logger } from "../lifecycle/index.js";
4
5
  import type { AcpConnection } from "./types.js";
5
6
  /**
6
7
  * Everything the workspace learns by subscribing to the connection: the Session row
@@ -13,8 +14,10 @@ export type AcpSessionStoreOptions = {
13
14
  now?: () => number;
14
15
  /** A live turn of a subscribed Session just stopped, however it ended. */
15
16
  onTurnFinished?: (sessionId: string) => void;
17
+ /** Where a listener that throws is told; the others still hear. */
18
+ logger?: Logger;
16
19
  };
17
- export declare function createAcpSessionStore({ connection, now, onTurnFinished, }: AcpSessionStoreOptions): {
20
+ export declare function createAcpSessionStore({ connection, now, onTurnFinished, logger, }: AcpSessionStoreOptions): {
18
21
  subscribe: (sessionId: string, agentId?: string) => () => void;
19
22
  dispose(): void;
20
23
  /**
@@ -36,6 +39,8 @@ export declare function createAcpSessionStore({ connection, now, onTurnFinished,
36
39
  createdAt?: string;
37
40
  platform?: import("./workspace.js").SessionPlatform;
38
41
  }[];
42
+ /** Marks Sessions a read has answered for, so a missing one gates no snapshot. */
43
+ answer(sessionIds: readonly string[]): void;
39
44
  /** The provider's title, as a list page or a subscribed Session reports it. */
40
45
  setTitle: (sessionId: string, title: string) => Map<string, string>;
41
46
  /** The proxy's read state, or the operator's own optimistic ack. */
@@ -14,6 +14,7 @@ import {
14
14
  HGW_STOP_REASONS2,
15
15
  HGW_PERMISSION_KIND_SESSION2,
16
16
  HGW_JSONRPC_ERRORS2,
17
+ HGW_MAX_FRAME_BYTES2,
17
18
  HgwInitializeMetaSchema2,
18
19
  HgwSessionSearchMetaSchema2,
19
20
  HgwSessionSearchResponseSchema2,
@@ -277,6 +278,13 @@ class AgentUpdateError extends Error {
277
278
  }
278
279
  }
279
280
 
281
+ class AcpConnectionClosedError extends Error {
282
+ constructor(message = "The ACP connection closed", options) {
283
+ super(message, options);
284
+ this.name = "AcpConnectionClosedError";
285
+ }
286
+ }
287
+
280
288
  // client/limits.ts
281
289
  var HANDSHAKE_DEADLINE_MS = 1e4;
282
290
  var REQUEST_DEADLINE_MS = {
@@ -370,6 +378,21 @@ function loggedStream(stream, logger) {
370
378
  })
371
379
  };
372
380
  }
381
+ var LOGGED_ID_LENGTH = 256;
382
+ function loggedId(row, ...path) {
383
+ let value = row;
384
+ for (const key of path)
385
+ value = typeof value === "object" && value !== null ? value[key] : undefined;
386
+ return typeof value === "string" && value.length > 0 ? value.slice(0, LOGGED_ID_LENGTH) : undefined;
387
+ }
388
+ function callEach(listeners, logger, event, ...args) {
389
+ for (const listener of [...listeners])
390
+ try {
391
+ listener(...args);
392
+ } catch (err) {
393
+ logger.warn({ err }, event);
394
+ }
395
+ }
373
396
 
374
397
  // client/connection.ts
375
398
  var TRY_AGAIN_LATER = 1013;
@@ -420,9 +443,6 @@ var SESSION_STATES = new Map([
420
443
  ["refused", "unavailable"],
421
444
  ["gone", "gone"]
422
445
  ]);
423
- function isTimeout(error) {
424
- return error instanceof DOMException && error.name === "TimeoutError";
425
- }
426
446
  function agentUpdateError(error) {
427
447
  if (codeOf(error) === HGW_JSONRPC_ERRORS2.unsupported)
428
448
  return new AgentUpdateError("unsupported", "This runtime cannot store that Agent field");
@@ -466,6 +486,9 @@ function trackedInbound(stream, onReceived) {
466
486
  writable: stream.writable
467
487
  };
468
488
  }
489
+ function closedUnder({ connection }, error) {
490
+ return connection.signal.aborted && error === connection.signal.reason;
491
+ }
469
492
  function observedSocket(Socket, onOpen, onClose) {
470
493
  return class extends Socket {
471
494
  constructor(...args) {
@@ -637,6 +660,35 @@ function sessionMachine({
637
660
  }
638
661
  });
639
662
  }
663
+ function readEach(entries, schema, skipped) {
664
+ const kept = [];
665
+ let unreadable;
666
+ for (const entry of entries) {
667
+ const read = schema.safeParse(entry);
668
+ if (read.success)
669
+ kept.push(read.data);
670
+ else {
671
+ unreadable = read.error;
672
+ skipped(entry);
673
+ }
674
+ }
675
+ if (entries.length > 0 && kept.length === 0)
676
+ throw unreadable;
677
+ return kept;
678
+ }
679
+ var AgentsListEntriesSchema = HgwAgentsListResponseSchema2.extend({
680
+ agents: z.array(z.unknown()).max(1000)
681
+ });
682
+ var SessionSearchEntriesSchema = HgwSessionSearchResponseSchema2.extend({
683
+ sessions: z.array(z.unknown())
684
+ });
685
+ var FRAME_ENVELOPE_BYTES = 1024;
686
+ var frameEncoder = new TextEncoder;
687
+ function assertFrameFits(params) {
688
+ const bytes = frameEncoder.encode(JSON.stringify(params)).byteLength;
689
+ if (bytes > HGW_MAX_FRAME_BYTES2 - FRAME_ENVELOPE_BYTES)
690
+ throw new RequestError(HGW_JSONRPC_ERRORS2.requestTooLarge, "Request too large to send");
691
+ }
640
692
  function createAcpConnection(options) {
641
693
  const {
642
694
  clientInfo,
@@ -660,6 +712,7 @@ function createAcpConnection(options) {
660
712
  let live;
661
713
  let recovering = false;
662
714
  let joinable = false;
715
+ let recovery;
663
716
  let invitation;
664
717
  let lastFocus;
665
718
  let silenceTimer = undefined;
@@ -674,23 +727,13 @@ function createAcpConnection(options) {
674
727
  if (status === next)
675
728
  return;
676
729
  status = next;
677
- for (const listener of statusListeners)
678
- listener(next);
730
+ callEach(statusListeners, logger, "acp.listener.failed", next);
679
731
  }
680
732
  function setOutage(next) {
681
733
  if (outage === next)
682
734
  return;
683
735
  outage = next;
684
- for (const listener of outageListeners)
685
- listener(next);
686
- }
687
- function settleOutage() {
688
- if (status !== "ready")
689
- return;
690
- for (const open of sessions.values())
691
- if (open.state === "joining")
692
- return;
693
- setOutage(undefined);
736
+ callEach(outageListeners, logger, "acp.listener.failed", next);
694
737
  }
695
738
  function emitPending(request) {
696
739
  for (const listener of pendingListeners)
@@ -708,7 +751,9 @@ function createAcpConnection(options) {
708
751
  sendLivenessProbeAsync();
709
752
  }, LIVENESS_SILENCE_MS);
710
753
  }
754
+ let lastInboundAt = Number.NEGATIVE_INFINITY;
711
755
  function onInbound() {
756
+ lastInboundAt = clock.now();
712
757
  armSilenceTimer();
713
758
  }
714
759
  function sendLivenessProbeAsync() {
@@ -766,14 +811,13 @@ function createAcpConnection(options) {
766
811
  if (open.replaying)
767
812
  open.replaying.delivered = true;
768
813
  for (const { listener } of open.listeners)
769
- listener.update?.(params.update, meta);
814
+ callEach(listener.update ? [listener.update] : [], logger, "acp.listener.failed", params.update, meta);
770
815
  }).onRequest(methods2.client.session.requestPermission, ({ params, signal }) => permissionRequest(params, signal)).onRequest(methods2.client.elicitation.create, ({ params, signal }) => elicitationRequest(params, signal));
771
816
  for (const [method, parser] of Object.entries(NOTIFICATION_PARSERS))
772
817
  app.onNotification(method, parser, ({ params }) => {
773
818
  if (method === HGW_METHODS2.notify.error)
774
819
  noticeGone(params);
775
- for (const listener of notificationListeners.get(method) ?? [])
776
- listener(params);
820
+ callEach(notificationListeners.get(method) ?? [], logger, "acp.listener.failed", params);
777
821
  });
778
822
  function noticeGone(params) {
779
823
  const notice = HgwErrorNotificationSchema2.safeParse(params).data;
@@ -781,48 +825,64 @@ function createAcpConnection(options) {
781
825
  return;
782
826
  sessions.get(notice.sessionId)?.owner?.actor.send({ type: "gone" });
783
827
  }
784
- async function readyTransport() {
828
+ async function liveTransport() {
785
829
  start();
786
830
  if (inspecting)
787
831
  await inspecting;
788
- const current = live;
789
- if (ended || !current)
790
- throw new Error("The ACP connection is not open");
832
+ if (ended || !live)
833
+ throw new AcpConnectionClosedError("The ACP connection is not open");
834
+ return live;
835
+ }
836
+ async function readyTransport() {
837
+ const current = await liveTransport();
791
838
  await current.ready;
792
839
  return current;
793
840
  }
794
- async function request(tier, send) {
795
- return requestOn(await readyTransport(), tier, send);
841
+ function recovered() {
842
+ start();
843
+ if (ended)
844
+ return Promise.reject(new AcpConnectionClosedError);
845
+ if (joinable)
846
+ return Promise.resolve();
847
+ recovery ??= Promise.withResolvers();
848
+ return recovery.promise;
849
+ }
850
+ async function request(tier, send, resend) {
851
+ const sessionId = resend === "read" ? undefined : resend?.sessionId;
852
+ for (;; ) {
853
+ if (sessionId !== undefined && sessions.has(sessionId))
854
+ await joined(sessionId);
855
+ else if (resend)
856
+ await recovered();
857
+ const transport = await liveTransport();
858
+ try {
859
+ await transport.ready;
860
+ return await requestOn(transport, tier, send);
861
+ } catch (error) {
862
+ if (!closedUnder(transport, error))
863
+ throw error;
864
+ const again = resend === "read" || sessionId !== undefined && sessions.has(sessionId);
865
+ if (!again)
866
+ throw new AcpConnectionClosedError(undefined, { cause: error });
867
+ await transport.connection.closed.catch((err) => logger.warn({ err }, "acp.connection.closed_with_error"));
868
+ }
869
+ }
796
870
  }
797
871
  async function requestOn({ connection }, tier, send, parent) {
798
872
  const deadline = new Deadline(REQUEST_DEADLINE_MS[tier], clock, parent);
873
+ const sentAt = clock.now();
799
874
  try {
800
875
  return await deadline.run((cancellationSignal) => send(connection.agent, { cancellationSignal }));
801
876
  } catch (error) {
802
877
  if (deadline.signal.aborted && !parent?.aborted) {
803
- if (tier === "probe")
804
- connection.close(error);
805
- else
878
+ if (tier !== "probe")
806
879
  sendLivenessProbeAsync();
880
+ else if (lastInboundAt <= sentAt)
881
+ connection.close(error);
807
882
  }
808
883
  throw error;
809
884
  }
810
885
  }
811
- async function write(sessionId, tier, send) {
812
- for (;; ) {
813
- if (sessions.has(sessionId))
814
- await joined(sessionId);
815
- const transport = await readyTransport();
816
- try {
817
- return await requestOn(transport, tier, send);
818
- } catch (error) {
819
- const closedUnder = transport.connection.signal.aborted && !isTimeout(error) && sessions.has(sessionId);
820
- if (!closedUnder)
821
- throw error;
822
- await transport.connection.closed.catch((err) => logger.warn({ err }, "acp.connection.closed_with_error"));
823
- }
824
- }
825
- }
826
886
  function notifyAgent(send, sessionId) {
827
887
  (async () => {
828
888
  if (sessionId !== undefined && sessions.has(sessionId))
@@ -921,7 +981,11 @@ function createAcpConnection(options) {
921
981
  }
922
982
  }
923
983
  async function listAgents() {
924
- return HgwAgentsListResponseSchema2.parse(await request("short", (agent, options) => agent.request(HGW_METHODS2.agents.list, undefined, options)));
984
+ const response = AgentsListEntriesSchema.parse(await request("short", (agent, options) => agent.request(HGW_METHODS2.agents.list, undefined, options), "read"));
985
+ return {
986
+ ...response,
987
+ agents: readEach(response.agents, HgwAgentsListResponseSchema2.shape.agents.element, (entry) => logger.warn({ id: loggedId(entry, "summary", "id") }, "agents.entry_skipped"))
988
+ };
925
989
  }
926
990
  function settleOf({ listener }) {
927
991
  const settle = listener.replay?.();
@@ -976,8 +1040,7 @@ function createAcpConnection(options) {
976
1040
  if (next === "joined" || failure !== undefined)
977
1041
  open.waiters = undefined;
978
1042
  for (const { listener } of open.listeners)
979
- listener.state?.(next);
980
- settleOutage();
1043
+ callEach(listener.state ? [listener.state] : [], logger, "acp.listener.failed", next);
981
1044
  }
982
1045
  function subscribe(sessionId, listener) {
983
1046
  if (ended)
@@ -1028,7 +1091,6 @@ function createAcpConnection(options) {
1028
1091
  open.owner?.dispose();
1029
1092
  open.waiters?.reject(notOpen());
1030
1093
  open.waiters = undefined;
1031
- settleOutage();
1032
1094
  if (open.state === "gone")
1033
1095
  return;
1034
1096
  request("short", (agent, options) => agent.request(HGW_METHODS2.session.part, { sessionId }, options)).catch((err) => {
@@ -1036,18 +1098,20 @@ function createAcpConnection(options) {
1036
1098
  });
1037
1099
  }
1038
1100
  async function resumePage(sessionId, cursor) {
1039
- await joined(sessionId);
1040
1101
  const open = sessions.get(sessionId);
1041
1102
  if (!open)
1042
1103
  throw notOpen();
1043
- const updates = [];
1044
- open.page = updates;
1104
+ let updates = [];
1045
1105
  try {
1046
- const response = await request("medium", (agent, options) => agent.request(methods2.agent.session.resume, {
1047
- sessionId,
1048
- cwd: AGENT_CWD,
1049
- replayFrom: { type: HGW_REPLAY_BEFORE2, cursor }
1050
- }, options));
1106
+ const response = await request("medium", (agent, options) => {
1107
+ updates = [];
1108
+ open.page = updates;
1109
+ return agent.request(methods2.agent.session.resume, {
1110
+ sessionId,
1111
+ cwd: AGENT_CWD,
1112
+ replayFrom: { type: HGW_REPLAY_BEFORE2, cursor }
1113
+ }, options);
1114
+ }, { sessionId });
1051
1115
  const { history } = HgwHistoryPageResponseMetaSchema2.parse(hgwMetaOf(response._meta));
1052
1116
  return { updates, history };
1053
1117
  } finally {
@@ -1063,6 +1127,9 @@ function createAcpConnection(options) {
1063
1127
  sendLivenessProbeAsync();
1064
1128
  }
1065
1129
  joinable = true;
1130
+ setOutage(undefined);
1131
+ recovery?.resolve();
1132
+ recovery = undefined;
1066
1133
  const opened = [...sessions.keys()];
1067
1134
  for (const open of sessions.values())
1068
1135
  open.owner?.actor.send({ type: "ready" });
@@ -1102,7 +1169,6 @@ function createAcpConnection(options) {
1102
1169
  closeTransport: () => live?.connection.close(),
1103
1170
  markReady() {
1104
1171
  setStatus("ready");
1105
- settleOutage();
1106
1172
  },
1107
1173
  lose(next) {
1108
1174
  recovering = true;
@@ -1165,13 +1231,15 @@ function createAcpConnection(options) {
1165
1231
  ended = true;
1166
1232
  setStatus("closed");
1167
1233
  setOutage(undefined);
1168
- failInitialized?.(new Error("The ACP connection closed"));
1234
+ failInitialized?.(new AcpConnectionClosedError);
1169
1235
  failInitialized = undefined;
1170
1236
  settleInitialized = undefined;
1237
+ recovery?.reject(new AcpConnectionClosedError);
1238
+ recovery = undefined;
1171
1239
  for (const open of sessions.values()) {
1172
1240
  clock.clearTimeout(open.grace);
1173
1241
  open.owner?.dispose();
1174
- open.waiters?.reject(new Error("The ACP connection closed"));
1242
+ open.waiters?.reject(new AcpConnectionClosedError);
1175
1243
  }
1176
1244
  sessions.clear();
1177
1245
  live?.connection.close();
@@ -1208,16 +1276,22 @@ function createAcpConnection(options) {
1208
1276
  const response = await request("short", (agent, options) => agent.request(methods2.agent.session.list, {
1209
1277
  ...cursor === undefined ? {} : { cursor },
1210
1278
  _meta: { [HGW_META_KEY2]: meta }
1211
- }, options));
1279
+ }, options), "read");
1212
1280
  return {
1213
1281
  sessions: response.sessions,
1214
1282
  ...response.nextCursor ? { nextCursor: response.nextCursor } : {}
1215
1283
  };
1216
1284
  },
1217
1285
  async searchSessions(meta) {
1218
- return HgwSessionSearchResponseSchema2.parse(await request("short", (agent, options) => agent.request(HGW_METHODS2.sessions.search, {
1219
- _meta: { [HGW_META_KEY2]: HgwSessionSearchMetaSchema2.parse(meta) }
1220
- }, options)));
1286
+ const response = SessionSearchEntriesSchema.parse(await request("short", (agent, options) => agent.request(HGW_METHODS2.sessions.search, {
1287
+ _meta: {
1288
+ [HGW_META_KEY2]: HgwSessionSearchMetaSchema2.parse(meta)
1289
+ }
1290
+ }, options), "read"));
1291
+ return {
1292
+ ...response,
1293
+ sessions: readEach(response.sessions, HgwSessionSearchResponseSchema2.shape.sessions.element, (entry) => logger.warn({ id: loggedId(entry, "sessionId") }, "sessions.result_skipped"))
1294
+ };
1221
1295
  },
1222
1296
  subscribe,
1223
1297
  joined,
@@ -1226,14 +1300,20 @@ function createAcpConnection(options) {
1226
1300
  resumePage,
1227
1301
  history: (sessionId) => sessions.get(sessionId)?.history,
1228
1302
  async prompt(sessionId, blocks, meta) {
1229
- const response = await write(sessionId, "long", (agent, options) => agent.request(methods2.agent.session.prompt, { sessionId, prompt: blocks, _meta: { [HGW_META_KEY2]: meta } }, options));
1303
+ const params = {
1304
+ sessionId,
1305
+ prompt: blocks,
1306
+ _meta: { [HGW_META_KEY2]: meta }
1307
+ };
1308
+ assertFrameFits(params);
1309
+ const response = await request("long", (agent, options) => agent.request(methods2.agent.session.prompt, params, options), { sessionId });
1230
1310
  return { messageId: response.messageId };
1231
1311
  },
1232
1312
  cancel(sessionId) {
1233
1313
  notifyAgent((agent) => agent.notify(methods2.agent.session.cancel, { sessionId }), sessionId);
1234
1314
  },
1235
1315
  async setConfigOption(sessionId, configId, value) {
1236
- const response = await write(sessionId, "medium", (agent, options) => agent.request(methods2.agent.session.setConfigOption, { sessionId, configId, type: "id", value }, options));
1316
+ const response = await request("medium", (agent, options) => agent.request(methods2.agent.session.setConfigOption, { sessionId, configId, type: "id", value }, options), { sessionId });
1237
1317
  return response.configOptions;
1238
1318
  },
1239
1319
  async deleteSession(sessionId) {
@@ -1243,7 +1323,8 @@ function createAcpConnection(options) {
1243
1323
  await request("short", (agent, options) => agent.request(HGW_METHODS2.session.update, update, options));
1244
1324
  },
1245
1325
  async steer(steer) {
1246
- return HgwSteerResponseSchema2.parse(await write(steer.sessionId, "medium", (agent, options) => agent.request(HGW_METHODS2.session.steer, steer, options)));
1326
+ assertFrameFits(steer);
1327
+ return HgwSteerResponseSchema2.parse(await request("medium", (agent, options) => agent.request(HGW_METHODS2.session.steer, steer, options), { sessionId: steer.sessionId }));
1247
1328
  },
1248
1329
  focus(sessionId, presence) {
1249
1330
  const report = {
@@ -1558,7 +1639,8 @@ function sameRow(left, right) {
1558
1639
  function createAcpSessionStore({
1559
1640
  connection,
1560
1641
  now = Date.now,
1561
- onTurnFinished
1642
+ onTurnFinished,
1643
+ logger = SILENT_LOGGER
1562
1644
  }) {
1563
1645
  const rows = new Map;
1564
1646
  const titles = new Map;
@@ -1577,11 +1659,12 @@ function createAcpSessionStore({
1577
1659
  return row ? [{ ...row }] : [];
1578
1660
  });
1579
1661
  }
1662
+ const answered = new Set;
1580
1663
  function notify(subscription) {
1581
1664
  for (const sessionId of subscription.sessionIds)
1582
- if (!rows.has(sessionId))
1665
+ if (!rows.has(sessionId) && !answered.has(sessionId))
1583
1666
  return;
1584
- subscription.listener(rowsFor(subscription.sessionIds));
1667
+ callEach([subscription.listener], logger, "sessions.listener.failed", rowsFor(subscription.sessionIds));
1585
1668
  }
1586
1669
  function publish(sessionId) {
1587
1670
  for (const subscription of subscriptions)
@@ -1623,15 +1706,13 @@ function createAcpSessionStore({
1623
1706
  function setTodos(sessionId, next) {
1624
1707
  todos.set(sessionId, next);
1625
1708
  for (const listener of todoListeners.get(sessionId) ?? [])
1626
- listener(next.map((todo) => ({ ...todo })));
1709
+ callEach([listener], logger, "sessions.listener.failed", next.map((todo) => ({ ...todo })));
1627
1710
  }
1628
1711
  function emitActivity(event) {
1629
- for (const listener of activityListeners)
1630
- listener(event);
1712
+ callEach(activityListeners, logger, "sessions.listener.failed", event);
1631
1713
  }
1632
1714
  function invalidate(sessionId) {
1633
- for (const listener of invalidationListeners.get(sessionId) ?? [])
1634
- listener();
1715
+ callEach(invalidationListeners.get(sessionId) ?? [], logger, "sessions.listener.failed");
1635
1716
  }
1636
1717
  function acceptUpdate(sessionId, update, meta) {
1637
1718
  if (SessionUpdate2.isStateUpdate(update)) {
@@ -1726,6 +1807,13 @@ function createAcpSessionStore({
1726
1807
  },
1727
1808
  put,
1728
1809
  rowsFor,
1810
+ answer(sessionIds) {
1811
+ for (const sessionId of sessionIds)
1812
+ answered.add(sessionId);
1813
+ for (const subscription of subscriptions)
1814
+ if (sessionIds.some((id) => subscription.sessionIds.has(id)))
1815
+ notify(subscription);
1816
+ },
1729
1817
  setTitle: (sessionId, title) => titles.set(sessionId, title),
1730
1818
  setUnread: (sessionId, unread) => patch(sessionId, { unread }),
1731
1819
  setPinned: (sessionId, pinned) => patch(sessionId, { pinned }),
@@ -1807,6 +1895,7 @@ function createAcpWorkspaceClient({
1807
1895
  }) {
1808
1896
  const store = createAcpSessionStore({
1809
1897
  connection,
1898
+ logger: clientLog,
1810
1899
  ...now ? { now } : {},
1811
1900
  onTurnFinished: (sessionId) => {
1812
1901
  reportCreatedAgents(sessionId).catch((err) => clientLog.warn({ err }, "agent.report_created_failed"));
@@ -1853,12 +1942,25 @@ function createAcpWorkspaceClient({
1853
1942
  return response;
1854
1943
  }
1855
1944
  function rememberRows(sessions) {
1945
+ const kept = [];
1946
+ let unreadable;
1856
1947
  for (const session of sessions) {
1857
- const row = rowOf(session);
1948
+ let row;
1949
+ try {
1950
+ row = rowOf(session);
1951
+ } catch (error) {
1952
+ unreadable = error;
1953
+ clientLog.warn({ id: loggedId(session, "sessionId") }, "sessions.row_skipped");
1954
+ continue;
1955
+ }
1858
1956
  remember(row.sessionId, row.info, row.updatedAt);
1859
1957
  if (row.title)
1860
1958
  store.setTitle(row.sessionId, row.title);
1959
+ kept.push(session);
1861
1960
  }
1961
+ if (sessions.length > 0 && kept.length === 0)
1962
+ throw unreadable;
1963
+ return kept;
1862
1964
  }
1863
1965
  const pageReads = new Map;
1864
1966
  let catalogScope;
@@ -1867,10 +1969,7 @@ function createAcpWorkspaceClient({
1867
1969
  const inFlight = pageReads.get(key);
1868
1970
  if (inFlight)
1869
1971
  return inFlight;
1870
- const read = connection.listSessions(meta, cursor).then((page) => {
1871
- rememberRows(page.sessions);
1872
- return page;
1873
- }).finally(() => pageReads.delete(key));
1972
+ const read = connection.listSessions(meta, cursor).then((page) => ({ ...page, sessions: rememberRows(page.sessions) })).finally(() => pageReads.delete(key));
1874
1973
  pageReads.set(key, read);
1875
1974
  return read;
1876
1975
  }
@@ -1938,16 +2037,16 @@ function createAcpWorkspaceClient({
1938
2037
  let page;
1939
2038
  try {
1940
2039
  page = await listSessions();
1941
- } catch {
2040
+ } catch (err) {
1942
2041
  if (stale(generation))
1943
2042
  return;
2043
+ clientLog.warn({ err, attempt }, "sessions.relist_retry_failed");
1944
2044
  attempt += 1;
1945
2045
  await backOff(backoffDelay(attempt, RELIST_BACKOFF));
1946
2046
  continue;
1947
2047
  }
1948
2048
  const sessionIds = page.sessions.map(({ sessionId }) => sessionId);
1949
- for (const listener of catalogListeners)
1950
- listener(sessionIds);
2049
+ callEach(catalogListeners, clientLog, "sessions.catalog_listener.failed", sessionIds);
1951
2050
  return;
1952
2051
  }
1953
2052
  }
@@ -2048,9 +2147,9 @@ function createAcpWorkspaceClient({
2048
2147
  query,
2049
2148
  ...agentId === undefined ? {} : { agentId }
2050
2149
  });
2051
- rememberRows(sessions);
2052
- const titles = new Map(sessions.map(({ sessionId, title }) => [sessionId, title ?? ""]));
2053
- return store.rowsFor(sessions.map(({ sessionId }) => sessionId)).map(({ sessionId, agentId, updatedAt }) => ({
2150
+ const kept = rememberRows(sessions);
2151
+ const titles = new Map(kept.map(({ sessionId, title }) => [sessionId, title ?? ""]));
2152
+ return store.rowsFor(kept.map(({ sessionId }) => sessionId)).map(({ sessionId, agentId, updatedAt }) => ({
2054
2153
  sessionId,
2055
2154
  agentId,
2056
2155
  title: titles.get(sessionId) ?? "",
@@ -2063,6 +2162,7 @@ function createAcpWorkspaceClient({
2063
2162
  },
2064
2163
  async getSessionMetadata(sessionIds) {
2065
2164
  await readRows(sessionIds);
2165
+ store.answer(sessionIds);
2066
2166
  return store.rowsFor(sessionIds);
2067
2167
  },
2068
2168
  subscribeSessionMetadata: store.subscribeMetadata,
@@ -2613,6 +2713,7 @@ class HgwRemoteClient {
2613
2713
  }
2614
2714
  }
2615
2715
  export {
2716
+ AcpConnectionClosedError,
2616
2717
  AgentUpdateError,
2617
2718
  HgwClientError,
2618
2719
  HgwRemoteClient,
@@ -28,4 +28,15 @@ export declare function frameFields(direction: "in" | "out", message: unknown):
28
28
  };
29
29
  /** `stream` with one `acp.frame` debug line per message, each way. */
30
30
  export declare function loggedStream(stream: WireStream, logger: Logger): WireStream;
31
+ /**
32
+ * The id at `path` in a row not yet checked, bounded for a warning. A warning
33
+ * about a rejected row names it by this alone, never by its content.
34
+ */
35
+ export declare function loggedId(row: unknown, ...path: string[]): string | undefined;
36
+ /**
37
+ * Calls every listener with `args`, each on its own: one that throws is
38
+ * logged as `event` and costs only itself, never the listeners after it nor
39
+ * the caller that fanned out to them.
40
+ */
41
+ export declare function callEach<Args extends readonly unknown[]>(listeners: Iterable<(...args: Args) => void>, logger: Logger, event: string, ...args: Args): void;
31
42
  export {};
@@ -63,6 +63,13 @@ export type AcpSessionListener = {
63
63
  replay?: AcpSessionReplayListener;
64
64
  state?: (state: AcpSessionState) => void;
65
65
  };
66
+ /**
67
+ * A read (`listAgents`, `listSessions`, `searchSessions`, `resumePage`) waits
68
+ * out a reconnect and goes out again on the transport that recovers, and a
69
+ * write for an opened Session once it has rejoined. Any other call a
70
+ * transport closes under, and every call once the connection has ended,
71
+ * rejects as `AcpConnectionClosedError`.
72
+ */
66
73
  export interface AcpConnection {
67
74
  readonly status: AcpConnectionStatus;
68
75
  /**
@@ -75,7 +82,10 @@ export interface AcpConnection {
75
82
  /** Resolves with `InitializeResponse._meta.hgw` once the handshake settles. */
76
83
  readonly initialized: Promise<z.infer<typeof HgwInitializeMetaSchema>>;
77
84
  subscribeStatus(listener: (status: AcpConnectionStatus) => void): () => void;
78
- /** Set from a close until every resumed Session has rejoined. */
85
+ /**
86
+ * Set from a close until the transport is ready again. Each Session's
87
+ * rejoin is its own state (`sessionState`), not the connection's.
88
+ */
79
89
  readonly outage: AcpConnectionOutage | undefined;
80
90
  subscribeOutage(listener: (outage: AcpConnectionOutage | undefined) => void): () => void;
81
91
  /**
@@ -17,6 +17,15 @@ export declare class AgentUpdateError extends Error {
17
17
  readonly code: "unsupported" | "conflict";
18
18
  constructor(code: "unsupported" | "conflict", message: string);
19
19
  }
20
+ /**
21
+ * The ACP connection is gone for a request: it has ended, no transport is open
22
+ * to send on, or a transport closed under a request it does not send again.
23
+ * `cause` holds what that transport closed with, which a failed socket leaves
24
+ * a DOM `Event`. Gateway refusals and timeouts reject as they come.
25
+ */
26
+ export declare class AcpConnectionClosedError extends Error {
27
+ constructor(message?: string, options?: ErrorOptions);
28
+ }
20
29
  export type SessionStatus = "idle" | "running" | "waiting-for-input" | "failed" | "unknown";
21
30
  export type ActivityBase = {
22
31
  id: string;
@@ -90,7 +90,17 @@ export declare const HGW_JSONRPC_ERRORS: {
90
90
  readonly temporarilyUnavailable: -31013;
91
91
  readonly uncertainMutation: -31014;
92
92
  readonly unsupported: -31015;
93
+ /**
94
+ * Params too large for one frame. The client raises it before sending and
95
+ * the gateway never sends it, so it has no machine name on the wire.
96
+ */
97
+ readonly requestTooLarge: -31016;
93
98
  };
99
+ /**
100
+ * The largest inbound WebSocket frame the gateway takes, in bytes. A larger
101
+ * one closes the socket, and with it every Session on the tab.
102
+ */
103
+ export declare const HGW_MAX_FRAME_BYTES = 1100000;
94
104
  export type HgwJsonRpcErrorCode = (typeof HGW_JSONRPC_ERRORS)[keyof typeof HGW_JSONRPC_ERRORS];
95
105
  export declare const IdentifierSchema: z.ZodString;
96
106
  export declare const HgwExtensionsSchema: z.ZodObject<{
@@ -65,6 +65,7 @@ import {
65
65
  HGW_PERMISSION_KIND_SESSION2,
66
66
  HGW_PLAN_ID2,
67
67
  HGW_JSONRPC_ERRORS2,
68
+ HGW_MAX_FRAME_BYTES2,
68
69
  IdentifierSchema2,
69
70
  HgwExtensionsSchema2,
70
71
  HgwClientCapabilitiesMetaSchema2,
@@ -215,6 +216,7 @@ export {
215
216
  HGW_AUTH_METHOD_INVITE2 as HGW_AUTH_METHOD_INVITE,
216
217
  HGW_EXTENSION_VERSION2 as HGW_EXTENSION_VERSION,
217
218
  HGW_JSONRPC_ERRORS2 as HGW_JSONRPC_ERRORS,
219
+ HGW_MAX_FRAME_BYTES2 as HGW_MAX_FRAME_BYTES,
218
220
  HGW_META_KEY2 as HGW_META_KEY,
219
221
  HGW_METHODS2 as HGW_METHODS,
220
222
  HGW_PERMISSION_KIND_SESSION2 as HGW_PERMISSION_KIND_SESSION,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harness-gw/sdk",
3
- "version": "0.5.1",
3
+ "version": "0.6.1",
4
4
  "description": "A gateway that puts Hermes, OpenClaw, and OpenCode behind one ACP WebSocket, and the browser client and protocol SDK for it.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",