@ziggs-ai/api-client 0.33.1 → 0.34.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.
@@ -13,6 +13,7 @@ export interface SendPresentation {
13
13
  type: string;
14
14
  };
15
15
  workContext?: WorkContext;
16
+ delivery?: SendChatMessageResult['delivery'];
16
17
  }
17
18
  /**
18
19
  * Present a send as what it is: a message that now exists, addressed to
@@ -38,10 +38,14 @@ export function presentSendResult(result, env) {
38
38
  // What counted it, in the server's words: a stranger's free messages with a
39
39
  // listed agent run out, and the person writing needs to know before they do.
40
40
  const counted = result.coverage?.message ? ` ${result.coverage.message}` : '';
41
+ const routing = delivery.reachable
42
+ ? `Assigned to ${delivery.assignee ?? 'nobody'}; reachable: ${delivery.reachable}${delivery.reason ? ` (${delivery.reason})` : ''}.${delivery.lastRead ? ` Last inbox read: ${delivery.lastRead}.` : ''}`
43
+ : 'Mailbox routing is unconfirmed; a person may be covered by an assistant.';
41
44
  return {
42
45
  ...base,
43
- delivered: `Message ${result.messageId} was accepted into chat ${result.chatId}, addressed to ${destination}. Mailbox routing is unconfirmed; a person may be covered by an assistant. Accepted is not read and does not confirm a wake or reply.${counted}`,
46
+ delivered: `Message ${result.messageId} was accepted into chat ${result.chatId}, addressed to ${destination}. ${routing} Accepted is not read and does not confirm a wake or reply.${counted}`,
44
47
  to: delivery.to,
48
+ delivery,
45
49
  };
46
50
  }
47
51
  /**
@@ -1,5 +1,5 @@
1
1
  import type { ChatReadDto } from '@ziggs-ai/contracts';
2
- import type { MessageDelivery, SendChatMessageResult as WireSendChatMessageResult } from '@ziggs-ai/contracts';
2
+ import type { MessageDelivery as WireMessageDelivery, SendChatMessageResult as WireSendChatMessageResult } from '@ziggs-ai/contracts';
3
3
  import { type Creds } from '../types.js';
4
4
  /**
5
5
  * One room from `GET /chats/mine`, as the server sends it.
@@ -85,7 +85,12 @@ export interface SendChatMessageInput {
85
85
  * `wakesAgent` field that once rode along was removed for exactly that reason
86
86
  * and is not coming back.
87
87
  */
88
- export type { MessageDelivery } from '@ziggs-ai/contracts';
88
+ export type MessageDelivery = WireMessageDelivery & {
89
+ assignee?: string | null;
90
+ reachable?: 'yes' | 'no';
91
+ reason?: string;
92
+ lastRead?: string | null;
93
+ };
89
94
  /**
90
95
  * The send result as a caller here holds it.
91
96
  *
@@ -105,6 +105,8 @@ export declare class ContextReadClient {
105
105
  * intended. Send it on every read made while acting on a wake.
106
106
  */
107
107
  constructor(operatorKey: string, agentId?: string, baseUrl?: string, laneId?: string);
108
+ /** The same reader acting on one wake's lane. */
109
+ withLane(laneId: string | undefined): ContextReadClient;
108
110
  read<T = unknown>(type: ContextReadType, query: ContextReadQuery): Promise<ContextReadEnvelope<T>>;
109
111
  /**
110
112
  * Aggregated chat snapshot — `GET /context/snapshot?via=chat:<id>`. The
@@ -74,6 +74,12 @@ export class ContextReadClient {
74
74
  this.baseUrl = baseUrl || getBackendUrl();
75
75
  this.laneId = laneId;
76
76
  }
77
+ /** The same reader acting on one wake's lane. */
78
+ withLane(laneId) {
79
+ if (!laneId || laneId === this.laneId)
80
+ return this;
81
+ return new ContextReadClient(this.operatorKey, this.agentId, this.baseUrl, laneId);
82
+ }
77
83
  async read(type, query) {
78
84
  if (!query.via?.trim()) {
79
85
  throw new Error('ContextReadClient.read: via is required');
@@ -31,9 +31,11 @@ export declare function parseErrorCode(responseBody: string): string | null;
31
31
  export declare function parseRetryAfterMs(headers: {
32
32
  get(name: string): string | null;
33
33
  }): number | null;
34
+ export declare function isCoverageRefusal(code: string | null | undefined): boolean;
34
35
  /**
35
36
  * Throw the parsed failure as an {@link ApiError} (or {@link RateLimitedError}
36
- * for 429), preserving status, body, machine code, and Retry-After.
37
+ * for a 429 that is not a coverage refusal), preserving status, body, machine
38
+ * code, and Retry-After.
37
39
  */
38
40
  export declare function throwApiError(response: {
39
41
  status: number;
@@ -100,14 +100,34 @@ function clampRetryMs(ms) {
100
100
  return 1_000;
101
101
  return Math.min(MAX_RETRY_AFTER_MS, Math.max(1_000, Math.round(ms)));
102
102
  }
103
+ /**
104
+ * Coverage refusals name themselves. A 429 with one of these codes is not a
105
+ * wait-and-retry: the writer has to hire, or someone has to set the agreement
106
+ * up. The backend answers them as 403; this keeps a stale 429 from becoming a
107
+ * rate limit too.
108
+ */
109
+ const COVERAGE_REFUSALS = new Set([
110
+ 'CONVERSATION_ALLOWANCE_EXHAUSTED',
111
+ 'CONTACT_POLICY_REFUSED',
112
+ 'LISTING_PAUSED',
113
+ 'CONVERSATION_COVERAGE_REQUIRED',
114
+ 'CONVERSATION_COVERAGE_ENDED',
115
+ 'CONVERSATION_NOT_FUNDED',
116
+ 'CONVERSATION_SPONSORSHIP_REQUIRED',
117
+ 'TASK_ONLY_AGENT',
118
+ ]);
119
+ export function isCoverageRefusal(code) {
120
+ return !!code && COVERAGE_REFUSALS.has(code);
121
+ }
103
122
  /**
104
123
  * Throw the parsed failure as an {@link ApiError} (or {@link RateLimitedError}
105
- * for 429), preserving status, body, machine code, and Retry-After.
124
+ * for a 429 that is not a coverage refusal), preserving status, body, machine
125
+ * code, and Retry-After.
106
126
  */
107
127
  export function throwApiError(response, responseBody, defaultMessage) {
108
128
  const message = parseErrorMessage(responseBody, defaultMessage);
109
129
  const code = parseErrorCode(responseBody);
110
- if (response.status === 429) {
130
+ if (response.status === 429 && !isCoverageRefusal(code)) {
111
131
  throw new RateLimitedError(message, response.headers ? parseRetryAfterMs(response.headers) : null, responseBody, code);
112
132
  }
113
133
  throw new ApiError(message, response.status, responseBody, code);
@@ -1,5 +1,5 @@
1
1
  import { ApiError, RateLimitedError } from '../types.js';
2
- import { parseErrorCode, parseErrorMessage, parseRetryAfterMs, } from './apiError.js';
2
+ import { isCoverageRefusal, parseErrorCode, parseErrorMessage, parseRetryAfterMs, } from './apiError.js';
3
3
  /**
4
4
  * a 429 is not a generic failure, it is an instruction with a
5
5
  * deadline attached.
@@ -33,7 +33,7 @@ export function isRateLimited(err) {
33
33
  export function pollSurfaceError(label, res, body) {
34
34
  const message = parseErrorMessage(body, `${label} failed: ${res.status}`);
35
35
  const code = parseErrorCode(body);
36
- if (res.status === 429) {
36
+ if (res.status === 429 && !isCoverageRefusal(code)) {
37
37
  return new RateLimitedError(message, parseRetryAfterMs(res.headers), body, code);
38
38
  }
39
39
  return new ApiError(message, res.status, body, code);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziggs-ai/api-client",
3
- "version": "0.33.1",
3
+ "version": "0.34.1",
4
4
  "description": "HTTP and WebSocket client for the Ziggs backend API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",