assistant-cloud 0.1.41 → 0.1.43

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 (40) hide show
  1. package/README.md +3 -3
  2. package/dist/AssistantCloud.js +2 -2
  3. package/dist/AssistantCloud.js.map +1 -1
  4. package/dist/AssistantCloudAPI.d.ts.map +1 -1
  5. package/dist/AssistantCloudAPI.js +5 -5
  6. package/dist/AssistantCloudAPI.js.map +1 -1
  7. package/dist/AssistantCloudAuthStrategy.d.ts.map +1 -1
  8. package/dist/AssistantCloudAuthStrategy.js +68 -17
  9. package/dist/AssistantCloudAuthStrategy.js.map +1 -1
  10. package/dist/AssistantCloudRuns.d.ts +3 -13
  11. package/dist/AssistantCloudRuns.d.ts.map +1 -1
  12. package/dist/AssistantCloudRuns.js.map +1 -1
  13. package/dist/CloudMessagePersistence.d.ts.map +1 -1
  14. package/dist/CloudMessagePersistence.js +19 -11
  15. package/dist/CloudMessagePersistence.js.map +1 -1
  16. package/dist/generateThreadTitle.d.ts +15 -0
  17. package/dist/generateThreadTitle.d.ts.map +1 -0
  18. package/dist/generateThreadTitle.js +25 -0
  19. package/dist/generateThreadTitle.js.map +1 -0
  20. package/dist/index.d.ts +3 -1
  21. package/dist/index.js +3 -1
  22. package/dist/runTelemetry.d.ts +61 -0
  23. package/dist/runTelemetry.d.ts.map +1 -0
  24. package/dist/runTelemetry.js +82 -0
  25. package/dist/runTelemetry.js.map +1 -0
  26. package/package.json +5 -5
  27. package/src/AssistantCloud.ts +1 -1
  28. package/src/AssistantCloudAPI.ts +9 -8
  29. package/src/AssistantCloudAuthStrategy.ts +140 -46
  30. package/src/AssistantCloudRuns.ts +3 -14
  31. package/src/CloudMessagePersistence.ts +23 -19
  32. package/src/generateThreadTitle.test.ts +71 -0
  33. package/src/generateThreadTitle.ts +38 -0
  34. package/src/index.ts +10 -0
  35. package/src/runTelemetry.test.ts +171 -0
  36. package/src/runTelemetry.ts +144 -0
  37. package/src/tests/AssistantCloud.test.ts +39 -0
  38. package/src/tests/AssistantCloudAPI.test.ts +25 -0
  39. package/src/tests/AssistantCloudAuthStrategy.test.ts +284 -10
  40. package/src/tests/CloudMessagePersistence.test.ts +93 -0
@@ -43,6 +43,7 @@ describe("AssistantCloudAnonymousAuthStrategy", () => {
43
43
  });
44
44
 
45
45
  afterEach(() => {
46
+ vi.useRealTimers();
46
47
  vi.unstubAllGlobals();
47
48
  if (originalLocalStorageDescriptor) {
48
49
  Object.defineProperty(
@@ -83,7 +84,7 @@ describe("AssistantCloudAnonymousAuthStrategy", () => {
83
84
  expect(values.get(refreshTokenKey)).toBe(JSON.stringify(nextRefreshToken));
84
85
  expect(fetchMock).toHaveBeenCalledWith(
85
86
  `${baseUrl}/v1/auth/tokens/anonymous`,
86
- { method: "POST" },
87
+ { method: "POST", signal: expect.any(AbortSignal) },
87
88
  );
88
89
  });
89
90
 
@@ -106,6 +107,276 @@ describe("AssistantCloudAnonymousAuthStrategy", () => {
106
107
  expect(fetchMock).toHaveBeenCalledTimes(1);
107
108
  });
108
109
 
110
+ it("deduplicates anonymous token requests across strategy instances", async () => {
111
+ const values = new Map<string, string>();
112
+ installLocalStorage({
113
+ getItem: (key) => values.get(key) ?? null,
114
+ setItem: (key, value) => {
115
+ values.set(key, value);
116
+ },
117
+ removeItem: (key) => {
118
+ values.delete(key);
119
+ },
120
+ } as Storage);
121
+ const fetchMock = mockAnonymousTokenFetch();
122
+ const first = new AssistantCloudAnonymousAuthStrategy(baseUrl);
123
+ const second = new AssistantCloudAnonymousAuthStrategy(baseUrl);
124
+
125
+ await expect(
126
+ Promise.all([first.getAuthHeaders(), second.getAuthHeaders()]),
127
+ ).resolves.toEqual([
128
+ { Authorization: `Bearer ${accessToken}` },
129
+ { Authorization: `Bearer ${accessToken}` },
130
+ ]);
131
+ expect(fetchMock).toHaveBeenCalledTimes(1);
132
+ expect(values.get(refreshTokenKey)).toBe(JSON.stringify(refreshToken));
133
+ });
134
+
135
+ it("coordinates anonymous token requests across realms", async () => {
136
+ const values = new Map<string, string>();
137
+ installLocalStorage({
138
+ getItem: (key) => values.get(key) ?? null,
139
+ setItem: (key, value) => {
140
+ values.set(key, value);
141
+ },
142
+ removeItem: (key) => {
143
+ values.delete(key);
144
+ },
145
+ } as Storage);
146
+ let lockTail: Promise<unknown> = Promise.resolve();
147
+ const lockRequest = vi.fn(
148
+ (_name: string, callback: () => Promise<string | null>) => {
149
+ const request = lockTail.then(callback);
150
+ lockTail = request.then(
151
+ () => undefined,
152
+ () => undefined,
153
+ );
154
+ return request;
155
+ },
156
+ );
157
+ vi.stubGlobal("navigator", { locks: { request: lockRequest } });
158
+ const rotatedRefreshToken = { token: "r2", expires_at: "2099-01-01" };
159
+ const response = (refreshTokenValue: typeof refreshToken) => ({
160
+ ok: true,
161
+ json: vi.fn().mockResolvedValue({
162
+ access_token: accessToken,
163
+ refresh_token: refreshTokenValue,
164
+ }),
165
+ });
166
+ const fetchMock = vi
167
+ .fn()
168
+ .mockResolvedValueOnce(response(refreshToken))
169
+ .mockResolvedValueOnce(response(rotatedRefreshToken));
170
+ vi.stubGlobal("fetch", fetchMock);
171
+
172
+ const first = new AssistantCloudAnonymousAuthStrategy(baseUrl);
173
+ vi.resetModules();
174
+ const { AssistantCloudAnonymousAuthStrategy: Second } =
175
+ await import("../AssistantCloudAuthStrategy");
176
+
177
+ await expect(
178
+ Promise.all([
179
+ first.getAuthHeaders(),
180
+ new Second(baseUrl).getAuthHeaders(),
181
+ ]),
182
+ ).resolves.toEqual([
183
+ { Authorization: `Bearer ${accessToken}` },
184
+ { Authorization: `Bearer ${accessToken}` },
185
+ ]);
186
+ expect(fetchMock).toHaveBeenNthCalledWith(
187
+ 1,
188
+ `${baseUrl}/v1/auth/tokens/anonymous`,
189
+ { method: "POST", signal: expect.any(AbortSignal) },
190
+ );
191
+ expect(fetchMock).toHaveBeenNthCalledWith(
192
+ 2,
193
+ `${baseUrl}/v1/auth/tokens/refresh`,
194
+ {
195
+ method: "POST",
196
+ headers: { "Content-Type": "application/json" },
197
+ body: JSON.stringify({ refresh_token: refreshToken.token }),
198
+ signal: expect.any(AbortSignal),
199
+ },
200
+ );
201
+ expect(lockRequest).toHaveBeenCalledTimes(2);
202
+ expect(values.get(refreshTokenKey)).toBe(
203
+ JSON.stringify(rotatedRefreshToken),
204
+ );
205
+ });
206
+
207
+ it("retries shared anonymous token requests after a failure", async () => {
208
+ const values = new Map<string, string>();
209
+ installLocalStorage({
210
+ getItem: (key) => values.get(key) ?? null,
211
+ setItem: (key, value) => {
212
+ values.set(key, value);
213
+ },
214
+ removeItem: (key) => {
215
+ values.delete(key);
216
+ },
217
+ } as Storage);
218
+ const failure = new Error("authentication unavailable");
219
+ const fetchMock = vi
220
+ .fn()
221
+ .mockRejectedValueOnce(failure)
222
+ .mockResolvedValueOnce({
223
+ ok: true,
224
+ json: vi.fn().mockResolvedValue({
225
+ access_token: accessToken,
226
+ refresh_token: refreshToken,
227
+ }),
228
+ });
229
+ vi.stubGlobal("fetch", fetchMock);
230
+ const first = new AssistantCloudAnonymousAuthStrategy(baseUrl);
231
+ const second = new AssistantCloudAnonymousAuthStrategy(baseUrl);
232
+
233
+ await Promise.all([
234
+ expect(first.getAuthHeaders()).rejects.toBe(failure),
235
+ expect(second.getAuthHeaders()).rejects.toBe(failure),
236
+ ]);
237
+ expect(fetchMock).toHaveBeenCalledTimes(1);
238
+
239
+ await expect(
240
+ new AssistantCloudAnonymousAuthStrategy(baseUrl).getAuthHeaders(),
241
+ ).resolves.toEqual({ Authorization: `Bearer ${accessToken}` });
242
+ expect(fetchMock).toHaveBeenCalledTimes(2);
243
+ });
244
+
245
+ it("aborts timed out shared anonymous token requests before retrying", async () => {
246
+ vi.useFakeTimers();
247
+ const values = new Map<string, string>();
248
+ installLocalStorage({
249
+ getItem: (key) => values.get(key) ?? null,
250
+ setItem: (key, value) => {
251
+ values.set(key, value);
252
+ },
253
+ removeItem: (key) => {
254
+ values.delete(key);
255
+ },
256
+ } as Storage);
257
+ let requestSignal: AbortSignal | null | undefined;
258
+ const fetchMock = vi
259
+ .fn()
260
+ .mockImplementationOnce((_input: RequestInfo | URL, init?: RequestInit) =>
261
+ Promise.resolve({
262
+ ok: true,
263
+ json: () =>
264
+ new Promise<never>((_resolve, reject) => {
265
+ requestSignal = init?.signal;
266
+ requestSignal?.addEventListener(
267
+ "abort",
268
+ () => reject(requestSignal?.reason),
269
+ { once: true },
270
+ );
271
+ }),
272
+ } as Response),
273
+ )
274
+ .mockResolvedValueOnce({
275
+ ok: true,
276
+ json: vi.fn().mockResolvedValue({
277
+ access_token: accessToken,
278
+ refresh_token: refreshToken,
279
+ }),
280
+ });
281
+ vi.stubGlobal("fetch", fetchMock);
282
+ const first = new AssistantCloudAnonymousAuthStrategy(baseUrl);
283
+ const second = new AssistantCloudAnonymousAuthStrategy(baseUrl);
284
+
285
+ const firstRequest = expect(first.getAuthHeaders()).rejects.toThrow(
286
+ "Assistant Cloud anonymous token request timed out after 30000ms",
287
+ );
288
+ const secondRequest = expect(second.getAuthHeaders()).rejects.toThrow(
289
+ "Assistant Cloud anonymous token request timed out after 30000ms",
290
+ );
291
+ await vi.advanceTimersByTimeAsync(0);
292
+ expect(fetchMock).toHaveBeenCalledTimes(1);
293
+ expect(requestSignal).toBeInstanceOf(AbortSignal);
294
+
295
+ await vi.advanceTimersByTimeAsync(30_000);
296
+ await Promise.all([firstRequest, secondRequest]);
297
+ expect(requestSignal?.aborted).toBe(true);
298
+ expect(values.has(refreshTokenKey)).toBe(false);
299
+
300
+ await expect(
301
+ new AssistantCloudAnonymousAuthStrategy(baseUrl).getAuthHeaders(),
302
+ ).resolves.toEqual({ Authorization: `Bearer ${accessToken}` });
303
+ expect(fetchMock).toHaveBeenCalledTimes(2);
304
+ });
305
+
306
+ it("aborts timed out refresh requests without replacing the identity", async () => {
307
+ vi.useFakeTimers();
308
+ const values = new Map([[refreshTokenKey, JSON.stringify(refreshToken)]]);
309
+ installLocalStorage({
310
+ getItem: (key) => values.get(key) ?? null,
311
+ setItem: (key, value) => {
312
+ values.set(key, value);
313
+ },
314
+ removeItem: (key) => {
315
+ values.delete(key);
316
+ },
317
+ } as Storage);
318
+ let requestSignal: AbortSignal | null | undefined;
319
+ const rotatedRefreshToken = { token: "r2", expires_at: "2099-02-01" };
320
+ const fetchMock = vi
321
+ .fn()
322
+ .mockImplementationOnce(
323
+ (_input: RequestInfo | URL, init?: RequestInit) =>
324
+ new Promise<Response>((_resolve, reject) => {
325
+ requestSignal = init?.signal;
326
+ requestSignal?.addEventListener(
327
+ "abort",
328
+ () => reject(requestSignal?.reason),
329
+ { once: true },
330
+ );
331
+ }),
332
+ )
333
+ .mockResolvedValueOnce({
334
+ ok: true,
335
+ json: vi.fn().mockResolvedValue({
336
+ access_token: accessToken,
337
+ refresh_token: rotatedRefreshToken,
338
+ }),
339
+ });
340
+ vi.stubGlobal("fetch", fetchMock);
341
+ const strategy = new AssistantCloudAnonymousAuthStrategy(baseUrl);
342
+
343
+ const request = expect(strategy.getAuthHeaders()).rejects.toThrow(
344
+ "Assistant Cloud refresh token request timed out after 30000ms",
345
+ );
346
+ expect(requestSignal).toBeInstanceOf(AbortSignal);
347
+
348
+ await vi.advanceTimersByTimeAsync(30_000);
349
+ await request;
350
+ expect(requestSignal?.aborted).toBe(true);
351
+ expect(values.get(refreshTokenKey)).toBe(JSON.stringify(refreshToken));
352
+
353
+ await expect(strategy.getAuthHeaders()).resolves.toEqual({
354
+ Authorization: `Bearer ${accessToken}`,
355
+ });
356
+ expect(fetchMock).toHaveBeenCalledTimes(2);
357
+ expect(values.get(refreshTokenKey)).toBe(
358
+ JSON.stringify(rotatedRefreshToken),
359
+ );
360
+ });
361
+
362
+ it("keeps anonymous token requests independent without localStorage", async () => {
363
+ delete (globalThis as { localStorage?: Storage }).localStorage;
364
+ const lockRequest = vi.fn();
365
+ vi.stubGlobal("navigator", { locks: { request: lockRequest } });
366
+ const fetchMock = mockAnonymousTokenFetch();
367
+ const first = new AssistantCloudAnonymousAuthStrategy(baseUrl);
368
+ const second = new AssistantCloudAnonymousAuthStrategy(baseUrl);
369
+
370
+ await expect(
371
+ Promise.all([first.getAuthHeaders(), second.getAuthHeaders()]),
372
+ ).resolves.toEqual([
373
+ { Authorization: `Bearer ${accessToken}` },
374
+ { Authorization: `Bearer ${accessToken}` },
375
+ ]);
376
+ expect(fetchMock).toHaveBeenCalledTimes(2);
377
+ expect(lockRequest).not.toHaveBeenCalled();
378
+ });
379
+
109
380
  it("scopes anonymous refresh tokens by backend", async () => {
110
381
  const secondBaseUrl = "https://other.example.com";
111
382
  const values = new Map<string, string>();
@@ -140,20 +411,20 @@ describe("AssistantCloudAnonymousAuthStrategy", () => {
140
411
  });
141
412
  vi.stubGlobal("fetch", fetchMock);
142
413
 
143
- await new AssistantCloudAnonymousAuthStrategy(baseUrl).getAuthHeaders();
144
- await new AssistantCloudAnonymousAuthStrategy(
145
- secondBaseUrl,
146
- ).getAuthHeaders();
414
+ await Promise.all([
415
+ new AssistantCloudAnonymousAuthStrategy(baseUrl).getAuthHeaders(),
416
+ new AssistantCloudAnonymousAuthStrategy(secondBaseUrl).getAuthHeaders(),
417
+ ]);
147
418
 
148
419
  expect(fetchMock).toHaveBeenNthCalledWith(
149
420
  1,
150
421
  `${baseUrl}/v1/auth/tokens/anonymous`,
151
- { method: "POST" },
422
+ { method: "POST", signal: expect.any(AbortSignal) },
152
423
  );
153
424
  expect(fetchMock).toHaveBeenNthCalledWith(
154
425
  2,
155
426
  `${secondBaseUrl}/v1/auth/tokens/anonymous`,
156
- { method: "POST" },
427
+ { method: "POST", signal: expect.any(AbortSignal) },
157
428
  );
158
429
  expect(values.get(refreshTokenKey)).toBe(JSON.stringify(refreshToken));
159
430
  expect(values.get(`aui:refresh_token:${secondBaseUrl}`)).toBe(
@@ -206,12 +477,13 @@ describe("AssistantCloudAnonymousAuthStrategy", () => {
206
477
  method: "POST",
207
478
  headers: { "Content-Type": "application/json" },
208
479
  body: JSON.stringify({ refresh_token: refreshToken.token }),
480
+ signal: expect.any(AbortSignal),
209
481
  },
210
482
  );
211
483
  expect(fetchMock).toHaveBeenNthCalledWith(
212
484
  2,
213
485
  `${secondBaseUrl}/v1/auth/tokens/anonymous`,
214
- { method: "POST" },
486
+ { method: "POST", signal: expect.any(AbortSignal) },
215
487
  );
216
488
  expect(values.get(refreshTokenKey)).toBe(JSON.stringify(refreshToken));
217
489
  expect(values.get(`aui:refresh_token:${secondBaseUrl}`)).toBe(
@@ -270,12 +542,13 @@ describe("AssistantCloudAnonymousAuthStrategy", () => {
270
542
  method: "POST",
271
543
  headers: { "Content-Type": "application/json" },
272
544
  body: JSON.stringify({ refresh_token: scopedRefreshToken.token }),
545
+ signal: expect.any(AbortSignal),
273
546
  },
274
547
  );
275
548
  expect(fetchMock).toHaveBeenNthCalledWith(
276
549
  2,
277
550
  `${secondBaseUrl}/v1/auth/tokens/anonymous`,
278
- { method: "POST" },
551
+ { method: "POST", signal: expect.any(AbortSignal) },
279
552
  );
280
553
  expect(values.has("aui:refresh_token")).toBe(false);
281
554
  expect(values.get(`aui:refresh_token:${secondBaseUrl}`)).toBe(
@@ -470,6 +743,7 @@ describe("AssistantCloudAnonymousAuthStrategy", () => {
470
743
  method: "POST",
471
744
  headers: { "Content-Type": "application/json" },
472
745
  body: JSON.stringify({ refresh_token: refreshToken.token }),
746
+ signal: expect.any(AbortSignal),
473
747
  },
474
748
  );
475
749
  expect(setItem).not.toHaveBeenCalled();
@@ -538,7 +812,7 @@ describe("AssistantCloudAnonymousAuthStrategy", () => {
538
812
  expect(fetchMock).toHaveBeenNthCalledWith(
539
813
  2,
540
814
  `${baseUrl}/v1/auth/tokens/anonymous`,
541
- { method: "POST" },
815
+ { method: "POST", signal: expect.any(AbortSignal) },
542
816
  );
543
817
  expect(values.get(refreshTokenKey)).toBe(
544
818
  JSON.stringify(replacementRefreshToken),
@@ -108,6 +108,99 @@ describe("CloudMessagePersistence", () => {
108
108
  });
109
109
  });
110
110
 
111
+ it("deduplicates concurrent child appends while the parent is pending", async () => {
112
+ let resolveParent!: (value: { message_id: string }) => void;
113
+ vi.mocked(cloud.threads.messages.create)
114
+ .mockImplementationOnce(
115
+ () =>
116
+ new Promise((resolve) => {
117
+ resolveParent = resolve;
118
+ }),
119
+ )
120
+ .mockResolvedValue({ message_id: "remote-child" });
121
+
122
+ const parent = persistence.append("thread-1", "parent", null, "aui/v0", {
123
+ text: "parent",
124
+ });
125
+ const firstChild = persistence.append(
126
+ "thread-1",
127
+ "child",
128
+ "parent",
129
+ "aui/v0",
130
+ { text: "child" },
131
+ );
132
+ const secondChild = persistence.append(
133
+ "thread-1",
134
+ "child",
135
+ "parent",
136
+ "aui/v0",
137
+ { text: "child" },
138
+ );
139
+
140
+ expect(persistence.isPersisted("child")).toBe(true);
141
+ resolveParent({ message_id: "remote-parent" });
142
+ await Promise.all([parent, firstChild, secondChild]);
143
+
144
+ expect(cloud.threads.messages.create).toHaveBeenCalledTimes(2);
145
+ });
146
+
147
+ it("resolves remote IDs throughout a concurrent message chain", async () => {
148
+ let resolveParent!: (value: { message_id: string }) => void;
149
+ vi.mocked(cloud.threads.messages.create)
150
+ .mockImplementationOnce(
151
+ () =>
152
+ new Promise((resolve) => {
153
+ resolveParent = resolve;
154
+ }),
155
+ )
156
+ .mockResolvedValueOnce({ message_id: "remote-child" })
157
+ .mockResolvedValueOnce({ message_id: "remote-grandchild" });
158
+
159
+ const parent = persistence.append("thread-1", "parent", null, "aui/v0", {
160
+ text: "parent",
161
+ });
162
+ const child = persistence.append("thread-1", "child", "parent", "aui/v0", {
163
+ text: "child",
164
+ });
165
+ const grandchild = persistence.append(
166
+ "thread-1",
167
+ "grandchild",
168
+ "child",
169
+ "aui/v0",
170
+ { text: "grandchild" },
171
+ );
172
+
173
+ resolveParent({ message_id: "remote-parent" });
174
+ await Promise.all([parent, child, grandchild]);
175
+
176
+ expect(cloud.threads.messages.create).toHaveBeenCalledWith("thread-1", {
177
+ parent_id: "remote-parent",
178
+ format: "aui/v0",
179
+ content: { text: "child" },
180
+ });
181
+ expect(cloud.threads.messages.create).toHaveBeenCalledWith("thread-1", {
182
+ parent_id: "remote-child",
183
+ format: "aui/v0",
184
+ content: { text: "grandchild" },
185
+ });
186
+ });
187
+
188
+ it("re-appends a message after its mapping has settled", async () => {
189
+ vi.mocked(cloud.threads.messages.create)
190
+ .mockResolvedValueOnce({ message_id: "remote-1" })
191
+ .mockResolvedValueOnce({ message_id: "remote-2" });
192
+
193
+ await persistence.append("thread-1", "local-1", null, "aui/v0", {
194
+ text: "first",
195
+ });
196
+ await persistence.append("thread-1", "local-1", null, "aui/v0", {
197
+ text: "second",
198
+ });
199
+
200
+ expect(cloud.threads.messages.create).toHaveBeenCalledTimes(2);
201
+ expect(await persistence.getRemoteId("local-1")).toBe("remote-2");
202
+ });
203
+
111
204
  it("loaded messages are marked as persisted and not re-created", async () => {
112
205
  vi.mocked(cloud.threads.messages.list).mockResolvedValue({
113
206
  messages: [