@adaptiqai/deepseek-harness 0.2.2 → 0.2.4

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/lib/index.js CHANGED
@@ -140,7 +140,10 @@ function classifyFailure(status, raw, headers, model2) {
140
140
  return make(CODES.invalidRequest, `AdaptiqAI couldn't accept this request${said}.`, null);
141
141
  }
142
142
  if (effective !== null && effective >= 500) {
143
- return make(CODES.server, "AdaptiqAI couldn't answer just now. It will retry automatically.", "offline");
143
+ const who = model2 ?? "AdaptiqAI";
144
+ if (effective === 503)
145
+ return make(CODES.server, `${model2 ?? "This model"} is offline right now. It will retry automatically.`, "retrying");
146
+ return make(CODES.server, `${who} didn't answer just now. It will retry automatically.`, "retrying");
144
147
  }
145
148
  if (effective === null) {
146
149
  return make(CODES.server, detail.message !== null ? `AdaptiqAI stopped the reply: ${detail.message.slice(0, 200)}` : "AdaptiqAI stopped the reply.", null);
@@ -149,18 +152,29 @@ function classifyFailure(status, raw, headers, model2) {
149
152
  }
150
153
  function transportFailure(error) {
151
154
  const name2 = error instanceof Error ? error.name : "";
152
- const timeout = name2 === "TimeoutError";
155
+ const cause = error instanceof Error ? error.cause?.code : void 0;
156
+ const timeout = name2 === "TimeoutError" || cause === "UND_ERR_HEADERS_TIMEOUT" || cause === "UND_ERR_BODY_TIMEOUT";
153
157
  return {
154
158
  code: timeout ? CODES.timeout : CODES.transport,
155
- message: timeout ? "AdaptiqAI took too long to answer. It will retry automatically." : "AdaptiqAI couldn't be reached. Check your internet connection; the request will retry automatically.",
159
+ message: timeout ? "AdaptiqAI took too long to answer. It will retry automatically." : "AdaptiqAI can't be reached right now. It will retry automatically.",
156
160
  status: null,
157
161
  requestId: null,
158
162
  retryAfterMs: null,
159
163
  serverType: null,
160
164
  serverCode: null,
161
- notice: "offline"
165
+ notice: timeout ? "retrying" : "offline"
162
166
  };
163
167
  }
168
+ function failureReach(failure) {
169
+ if (failure.code === CODES.transport)
170
+ return "unreachable";
171
+ if (failure.code === CODES.timeout)
172
+ return "transient";
173
+ return failure.status !== null && failure.status >= 500 && failure.status !== 507 ? "transient" : null;
174
+ }
175
+ function errorReach(error) {
176
+ return error instanceof AdaptiqHttpError ? failureReach(error.failure) : null;
177
+ }
164
178
  var AdaptiqHttpError = class extends Error {
165
179
  failure;
166
180
  body;
@@ -699,6 +713,24 @@ function formatPercent(fraction) {
699
713
  return "<1%";
700
714
  return `${Math.min(100, Math.round(fraction * 100))}%`;
701
715
  }
716
+ var QUIET_START_MS = 2e3;
717
+ var LANES_FRESH_MS = 3e4;
718
+ function waitKind(request, lanes, refreshedAt, now) {
719
+ if (request.phase === "preparing")
720
+ return "preparing";
721
+ if (request.phase === "queued")
722
+ return "lane";
723
+ if (request.phase !== "starting")
724
+ return null;
725
+ if (request.since !== null && now - request.since < QUIET_START_MS)
726
+ return null;
727
+ const total = lanes?.lanes ?? null;
728
+ const running = lanes?.runningNow ?? null;
729
+ const fresh = refreshedAt !== null && now - refreshedAt <= LANES_FRESH_MS;
730
+ if (request.laneHeld !== true && fresh && total !== null && total > 0 && running !== null && running >= total)
731
+ return "lane";
732
+ return "starting";
733
+ }
702
734
 
703
735
  // ../core/lib/catalog.js
704
736
  var obj = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
@@ -1243,6 +1275,11 @@ var str4 = (value) => typeof value === "string" && value.trim() !== "" ? value.t
1243
1275
  var count2 = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
1244
1276
  var DEFAULT_PREFERENCES = { rememberByDefault: true, welcomed: false };
1245
1277
  var PAGE = 50;
1278
+ var TROUBLE_QUIET_MS = 6e4;
1279
+ var SESSION_BACKOFF_MAX_MS = 5 * 6e4;
1280
+ function sessionBackoffMs(failures) {
1281
+ return Math.min(SESSION_BACKOFF_MAX_MS, 3e4 * 2 ** Math.max(1, failures));
1282
+ }
1246
1283
  function copyKey(source, target) {
1247
1284
  return `${source}\0${target ?? ""}`;
1248
1285
  }
@@ -1418,6 +1455,10 @@ var Workspace = class {
1418
1455
  catalog = [];
1419
1456
  catalogKey = "";
1420
1457
  catalogAt = 0;
1458
+ /** The last `/v1/models` rows read: one stalled engine can fail the list for a while. */
1459
+ modelRows = [];
1460
+ /** Background reads have failed for a passing reason since `since`. */
1461
+ trouble = null;
1421
1462
  offers = [];
1422
1463
  checkoutMode = null;
1423
1464
  /** Offer id → what the account summary says about it (model id, console link). */
@@ -1516,8 +1557,10 @@ var Workspace = class {
1516
1557
  this.purchases.dispose();
1517
1558
  clearTimeout(this.timer);
1518
1559
  clearTimeout(this.attachTimer);
1519
- for (const session of this.sessions.values())
1560
+ for (const session of this.sessions.values()) {
1520
1561
  clearTimeout(session.timer);
1562
+ clearTimeout(session.waitTimer);
1563
+ }
1521
1564
  this.listeners.clear();
1522
1565
  }
1523
1566
  /** Record that the provider route belongs to someone else. */
@@ -1635,7 +1678,8 @@ var Workspace = class {
1635
1678
  return;
1636
1679
  this.timer = setTimeout(() => {
1637
1680
  void this.refresh().finally(() => {
1638
- this.schedule(this.listeners.size > 0 ? this.deps.watchedRefreshMs : this.deps.refreshMs);
1681
+ const normal = this.listeners.size > 0 ? this.deps.watchedRefreshMs : this.deps.refreshMs;
1682
+ this.schedule(this.trouble === null ? normal : Math.min(normal, 15e3 * 2 ** Math.min(this.trouble.failures - 1, 6)));
1639
1683
  });
1640
1684
  }, ms);
1641
1685
  this.timer.unref?.();
@@ -1670,8 +1714,17 @@ var Workspace = class {
1670
1714
  this.setCatalog([]);
1671
1715
  return;
1672
1716
  }
1673
- const rows = await this.deps.api.json("/v1/models");
1674
- const data = Array.isArray(rows?.data) ? rows.data : [];
1717
+ let data;
1718
+ try {
1719
+ const rows = await this.deps.api.json("/v1/models");
1720
+ data = Array.isArray(rows?.data) ? rows.data : [];
1721
+ this.modelRows = data;
1722
+ } catch (error) {
1723
+ if (errorReach(error) === null || this.modelRows.length === 0)
1724
+ throw error;
1725
+ this.deps.log?.("warn", `adaptiq: model list unavailable, keeping the last one: ${error instanceof Error ? error.message : String(error)}`);
1726
+ data = this.modelRows;
1727
+ }
1675
1728
  const summary2 = await this.loadSummary();
1676
1729
  let availability;
1677
1730
  if (summary2 !== null) {
@@ -1688,19 +1741,33 @@ var Workspace = class {
1688
1741
  if (this.memory === null || !this.memory.available)
1689
1742
  await this.loadFallbackMemory();
1690
1743
  this.lastError = null;
1744
+ this.trouble = null;
1691
1745
  this.refreshedAt = this.now();
1692
1746
  this.evaluateNotices();
1693
1747
  } catch (error) {
1694
1748
  const failure = error instanceof AdaptiqHttpError ? error.failure : null;
1695
1749
  this.lastError = { code: failure?.code ?? "UNKNOWN", message: failure?.message ?? (error instanceof Error ? error.message : String(error)), at: this.now() };
1696
- if (failure?.notice === "offline")
1697
- this.raise({ kind: "offline", tone: "warning", title: "AdaptiqAI can't be reached", body: "Your models stay listed; requests will work again once the connection is back.", actions: [{ id: "refresh", label: "Try again" }] });
1750
+ const reach = failure === null ? null : failureReach(failure);
1751
+ if (reach !== null) {
1752
+ const since = this.trouble?.since ?? this.now();
1753
+ this.trouble = { since, reach, failures: (this.trouble?.failures ?? 0) + 1 };
1754
+ if (this.now() - since >= TROUBLE_QUIET_MS)
1755
+ this.raiseTrouble(reach);
1756
+ }
1698
1757
  this.deps.log?.("warn", `adaptiq: refresh failed: ${this.lastError.message}`);
1699
1758
  } finally {
1700
1759
  this.loading = false;
1701
1760
  this.publish();
1702
1761
  }
1703
1762
  }
1763
+ /**
1764
+ * Say, calmly, that background reads keep failing: "busy" while AdaptiqAI
1765
+ * answers with errors, "can't be reached" only when there is no connection.
1766
+ * @param reach - the kind of trouble.
1767
+ */
1768
+ raiseTrouble(reach) {
1769
+ this.raise(reach === "unreachable" ? { kind: "offline", tone: "warning", title: "AdaptiqAI can't be reached", body: "Retrying automatically. Your models and conversations stay as they are.", actions: [{ id: "refresh", label: "Try again" }] } : { kind: "offline", tone: "info", title: "AdaptiqAI is busy \u2014 retrying", body: "Your models and conversations stay as they are. This usually clears by itself.", actions: [] });
1770
+ }
1704
1771
  setCatalog(models) {
1705
1772
  const key = JSON.stringify(models);
1706
1773
  this.catalogAt = this.now();
@@ -1713,8 +1780,13 @@ var Workspace = class {
1713
1780
  if (session.listeners.size === 0)
1714
1781
  continue;
1715
1782
  this.emitSession(session);
1716
- if (!session.exists)
1783
+ const awaited = session.backoff?.model ?? null;
1784
+ if (awaited !== null && !this.modelDown(awaited)) {
1785
+ session.backoff = null;
1786
+ this.pollSession(session, 0);
1787
+ } else if (!session.exists && session.remoteAt === 0) {
1717
1788
  this.pollSession(session, 0);
1789
+ }
1718
1790
  }
1719
1791
  }
1720
1792
  async loadSummary() {
@@ -2049,6 +2121,10 @@ var Workspace = class {
2049
2121
  onAdaptiq: route === void 0 ? null : route.provider === this.deps.route,
2050
2122
  phase: "idle",
2051
2123
  phaseSince: null,
2124
+ inflight: 0,
2125
+ laneHeld: false,
2126
+ retry: null,
2127
+ failedNow: false,
2052
2128
  model: route?.provider === this.deps.route ? route.model : null,
2053
2129
  selected: null,
2054
2130
  lastUsed: null,
@@ -2061,6 +2137,9 @@ var Workspace = class {
2061
2137
  knowledge: [],
2062
2138
  conversations: [],
2063
2139
  remoteAt: 0,
2140
+ home: null,
2141
+ notOn: /* @__PURE__ */ new Set(),
2142
+ backoff: null,
2064
2143
  listeners: /* @__PURE__ */ new Set()
2065
2144
  };
2066
2145
  this.sessions.set(id, session);
@@ -2090,6 +2169,7 @@ var Workspace = class {
2090
2169
  remember: this.remember(id),
2091
2170
  phase: session.phase,
2092
2171
  phaseSince: session.phaseSince,
2172
+ waiting: this.waiting(session),
2093
2173
  model: session.model,
2094
2174
  modelName: this.modelName(session.model),
2095
2175
  selectedModel: session.selected,
@@ -2137,13 +2217,29 @@ var Workspace = class {
2137
2217
  clearTimeout(session.timer);
2138
2218
  if (this.disposed || session.listeners.size === 0)
2139
2219
  return;
2220
+ const wait = session.backoff === null ? 0 : session.backoff.until - this.now();
2140
2221
  session.timer = setTimeout(() => {
2141
2222
  void this.loadSession(session).finally(() => {
2142
2223
  this.pollSession(session, sessionPollDelay(session));
2143
2224
  });
2144
- }, ms);
2225
+ }, Math.max(ms, wait));
2145
2226
  session.timer.unref?.();
2146
2227
  }
2228
+ /** @returns whether a model's instance is offline or disabled (not asked about conversations). */
2229
+ modelDown(id) {
2230
+ const status = this.catalog.find((entry) => entry.id === id)?.status;
2231
+ return status === "offline" || status === "disabled";
2232
+ }
2233
+ /**
2234
+ * Read a conversation less often after a failed read or while its model is
2235
+ * offline: 1, 2, 4, then every 5 minutes.
2236
+ * @param session - the conversation.
2237
+ * @param model - the offline model it waits for, or null after a failed read.
2238
+ */
2239
+ backOff(session, model2) {
2240
+ const failures = (session.backoff?.failures ?? 0) + 1;
2241
+ session.backoff = { failures, until: this.now() + sessionBackoffMs(failures), model: model2 };
2242
+ }
2147
2243
  /**
2148
2244
  * The memory store a conversation's knowledge and memory live in: where it is
2149
2245
  * stored once it exists, otherwise the model selected for it.
@@ -2230,42 +2326,23 @@ var Workspace = class {
2230
2326
  return;
2231
2327
  session.model = route.model;
2232
2328
  }
2233
- const path = `/v1/sessions/${encodeURIComponent(session.id)}`;
2234
- let body = null;
2235
- let routed = false;
2236
- let suggested = [];
2237
- try {
2238
- body = await this.deps.api.json(path, { allow: [404] });
2239
- routed = true;
2240
- } catch (error) {
2241
- if (!(error instanceof AdaptiqHttpError) || error.status !== 409 && error.status !== 400)
2242
- throw error;
2243
- const models = obj4(obj4(error.body)?.detail)?.models;
2244
- if (Array.isArray(models))
2245
- suggested = models.filter((model2) => typeof model2 === "string" && model2 !== "");
2246
- }
2247
- const candidates = routed ? [] : session.model !== null ? [session.model] : suggested.length > 0 ? suggested.slice(0, 6) : [...this.catalog].sort((a, b) => Number(b.status === "running") - Number(a.status === "running")).map((entry) => entry.id).slice(0, 6);
2248
- for (const model2 of candidates) {
2249
- body = await this.deps.api.json(path, { headers: { "x-adaptiq-model": model2 }, allow: [404] });
2250
- if (body !== null) {
2251
- if (session.model === null) {
2252
- session.model = model2;
2253
- session.onAdaptiq ??= true;
2254
- }
2255
- break;
2256
- }
2257
- }
2258
- if (routed && body !== null) {
2259
- const served = str4(obj4(body.session)?.model) ?? str4(body.model);
2260
- if (served !== null)
2261
- session.model = served;
2262
- else if (session.model === null && session.lastUsed !== null)
2263
- session.model = session.lastUsed;
2264
- session.onAdaptiq ??= true;
2329
+ const found = await this.findSession(session);
2330
+ if (found === void 0) {
2331
+ session.remoteAt = this.now();
2332
+ return;
2265
2333
  }
2266
- if (routed || candidates.length > 0)
2334
+ session.backoff = null;
2335
+ if (found.asked)
2267
2336
  session.remoteAt = this.now();
2337
+ const body = found.body;
2268
2338
  if (body === null) {
2339
+ if (session.exists) {
2340
+ session.memoryTokens = null;
2341
+ session.saved = null;
2342
+ session.optimizing = false;
2343
+ session.revision = null;
2344
+ session.conversations = [];
2345
+ }
2269
2346
  session.exists = false;
2270
2347
  session.knowledge = [];
2271
2348
  return;
@@ -2289,6 +2366,7 @@ var Workspace = class {
2289
2366
  session.conversations = await this.attachedConversations(session).catch(() => session.conversations);
2290
2367
  } catch (error) {
2291
2368
  session.remoteAt = this.now();
2369
+ this.backOff(session, null);
2292
2370
  this.deps.log?.("warn", `adaptiq: could not read conversation ${session.id}: ${error instanceof Error ? error.message : String(error)}`);
2293
2371
  if (!(error instanceof AdaptiqHttpError))
2294
2372
  throw error;
@@ -2304,6 +2382,83 @@ var Workspace = class {
2304
2382
  session.loading = void 0;
2305
2383
  }
2306
2384
  }
2385
+ /**
2386
+ * Find a conversation on the server. Once a model has answered for it, only
2387
+ * that model is asked. Before that, the session-aware route (which needs no
2388
+ * model), then, on gateways that want one, the conversation's model or the
2389
+ * models the gateway names: never a model that is offline or that already
2390
+ * said the conversation is not there.
2391
+ * @param session - the conversation.
2392
+ * @returns its body (null when the server does not have it) and whether
2393
+ * anything could be asked, or undefined when its model is offline (backed off).
2394
+ */
2395
+ async findSession(session) {
2396
+ const path = `/v1/sessions/${encodeURIComponent(session.id)}`;
2397
+ const ask = async (model2) => {
2398
+ try {
2399
+ return await this.deps.api.json(path, { headers: { "x-adaptiq-model": model2 }, allow: [404] });
2400
+ } catch (error) {
2401
+ if (error instanceof AdaptiqHttpError && error.status === 409)
2402
+ return null;
2403
+ throw error;
2404
+ }
2405
+ };
2406
+ if (session.home !== null) {
2407
+ const home = session.home;
2408
+ if (this.modelDown(home)) {
2409
+ this.backOff(session, home);
2410
+ return void 0;
2411
+ }
2412
+ const body = await ask(home);
2413
+ if (body === null) {
2414
+ session.home = null;
2415
+ session.notOn.add(home);
2416
+ } else
2417
+ session.model = home;
2418
+ return { body, asked: true };
2419
+ }
2420
+ let suggested = [];
2421
+ try {
2422
+ const body = await this.deps.api.json(path, { allow: [404] });
2423
+ if (body !== null) {
2424
+ const served = str4(obj4(body.session)?.model) ?? str4(body.model);
2425
+ if (served !== null) {
2426
+ session.model = served;
2427
+ session.home = served;
2428
+ } else if (session.model === null && session.lastUsed !== null)
2429
+ session.model = session.lastUsed;
2430
+ session.onAdaptiq ??= true;
2431
+ }
2432
+ return { body, asked: true };
2433
+ } catch (error) {
2434
+ if (!(error instanceof AdaptiqHttpError) || error.status !== 409 && error.status !== 400)
2435
+ throw error;
2436
+ const models = obj4(obj4(error.body)?.detail)?.models;
2437
+ if (Array.isArray(models))
2438
+ suggested = models.filter((model2) => typeof model2 === "string" && model2 !== "");
2439
+ }
2440
+ const listed = session.model !== null ? [session.model] : suggested.length > 0 ? suggested : [...this.catalog].sort((a, b) => Number(b.status === "running") - Number(a.status === "running")).map((entry) => entry.id);
2441
+ const open = listed.filter((model2) => !session.notOn.has(model2)).slice(0, 6);
2442
+ const candidates = open.filter((model2) => !this.modelDown(model2));
2443
+ for (const model2 of candidates) {
2444
+ const body = await ask(model2);
2445
+ if (body !== null) {
2446
+ session.home = model2;
2447
+ if (session.model === null) {
2448
+ session.model = model2;
2449
+ session.onAdaptiq ??= true;
2450
+ }
2451
+ return { body, asked: true };
2452
+ }
2453
+ session.notOn.add(model2);
2454
+ }
2455
+ const offline = open.find((model2) => this.modelDown(model2));
2456
+ if (offline !== void 0) {
2457
+ this.backOff(session, offline);
2458
+ return void 0;
2459
+ }
2460
+ return { body: null, asked: candidates.length > 0 };
2461
+ }
2307
2462
  /**
2308
2463
  * Choose whether new messages of a conversation are saved.
2309
2464
  * @param id - harness session id.
@@ -2322,6 +2477,38 @@ var Workspace = class {
2322
2477
  session.phase = phase;
2323
2478
  session.phaseSince = this.now();
2324
2479
  this.emitSession(session);
2480
+ this.watchWait(session);
2481
+ }
2482
+ /** What the conversation's request waits for, as the composer says it. */
2483
+ waiting(session) {
2484
+ const entry = this.catalog.find((item) => item.id === session.model);
2485
+ return waitKind({ phase: session.phase, since: session.phaseSince, laneHeld: session.laneHeld }, entry ?? null, this.refreshedAt, this.now());
2486
+ }
2487
+ /**
2488
+ * A request that stays quiet past {@link QUIET_START_MS} gets a word: look
2489
+ * again then, with the lane counts read fresh when they are older than that.
2490
+ */
2491
+ watchWait(session) {
2492
+ clearTimeout(session.waitTimer);
2493
+ if (session.phase !== "starting" || this.disposed)
2494
+ return;
2495
+ const check = () => {
2496
+ if (session.phase !== "starting" || this.disposed)
2497
+ return;
2498
+ this.emitSession(session);
2499
+ const stale = this.refreshedAt === null || this.now() - this.refreshedAt > 1e4;
2500
+ if (stale && !session.laneHeld && this.deps.auth.phase() === "connected") {
2501
+ void this.refresh().then(() => {
2502
+ if (session.phase === "starting")
2503
+ this.emitSession(session);
2504
+ }, () => {
2505
+ });
2506
+ }
2507
+ session.waitTimer = setTimeout(check, 15e3);
2508
+ session.waitTimer.unref?.();
2509
+ };
2510
+ session.waitTimer = setTimeout(check, QUIET_START_MS);
2511
+ session.waitTimer.unref?.();
2325
2512
  }
2326
2513
  // RequestEvents — the adapter reports each request here.
2327
2514
  started(sessionId2, model2) {
@@ -2330,7 +2517,13 @@ var Workspace = class {
2330
2517
  const session = this.session(sessionId2);
2331
2518
  session.onAdaptiq = true;
2332
2519
  session.model = model2;
2333
- this.setPhase(session, "preparing");
2520
+ session.notOn.delete(model2);
2521
+ session.backoff = null;
2522
+ session.inflight++;
2523
+ session.laneHeld = false;
2524
+ session.failedNow = false;
2525
+ const retry = session.retry !== null && session.retry.until > this.now() ? session.retry : null;
2526
+ this.setPhase(session, retry?.phase ?? "starting");
2334
2527
  }
2335
2528
  progress(sessionId2, stage) {
2336
2529
  if (sessionId2 === void 0)
@@ -2340,14 +2533,26 @@ var Workspace = class {
2340
2533
  return;
2341
2534
  if (stage === "inference.queued")
2342
2535
  this.setPhase(session, "queued");
2343
- else if (stage === "context.preparing" || stage === "request.accepted")
2536
+ else if (stage === "context.preparing")
2344
2537
  this.setPhase(session, "preparing");
2538
+ else if (stage === "context.ready" || stage === "inference.ready") {
2539
+ if (stage === "inference.ready")
2540
+ session.laneHeld = true;
2541
+ session.retry = null;
2542
+ if (session.phase === "starting")
2543
+ this.emitSession(session);
2544
+ else
2545
+ this.setPhase(session, "starting");
2546
+ }
2345
2547
  }
2346
2548
  output(sessionId2) {
2347
2549
  if (sessionId2 === void 0)
2348
2550
  return;
2349
- this.setPhase(this.session(sessionId2), "responding");
2350
- this.clearNotices("busy", "preparing", "offline");
2551
+ const session = this.session(sessionId2);
2552
+ session.laneHeld = true;
2553
+ session.retry = null;
2554
+ this.setPhase(session, "responding");
2555
+ this.clearNotices("busy", "preparing", "offline", "retrying");
2351
2556
  }
2352
2557
  usage(sessionId2, model2, usage2) {
2353
2558
  if (sessionId2 === void 0)
@@ -2382,8 +2587,13 @@ var Workspace = class {
2382
2587
  message: failure.message
2383
2588
  });
2384
2589
  this.failures.length = Math.min(this.failures.length, 12);
2385
- if (sessionId2 !== void 0)
2386
- this.setPhase(this.session(sessionId2), "interrupted");
2590
+ if (sessionId2 !== void 0) {
2591
+ const session = this.session(sessionId2);
2592
+ session.failedNow = true;
2593
+ const waits = RETRYABLE_CODES.includes(failure.code) && (failure.notice === "busy" || failure.notice === "preparing");
2594
+ session.retry = waits ? { phase: failure.notice === "busy" ? "queued" : "preparing", until: this.now() + (failure.retryAfterMs ?? 3e4) + 15e3 } : null;
2595
+ this.setPhase(session, session.retry?.phase ?? "interrupted");
2596
+ }
2387
2597
  const entry = this.catalog.find((item) => item.id === model2);
2388
2598
  const name2 = entry?.name ?? model2;
2389
2599
  switch (failure.notice) {
@@ -2425,8 +2635,20 @@ var Workspace = class {
2425
2635
  case "model-unavailable":
2426
2636
  this.raise({ kind: "model-unavailable", tone: "warning", title: `${name2} isn't available`, body: failure.message, actions: [{ id: "open-models", label: "See your models", url: this.links.models }] });
2427
2637
  break;
2638
+ case "retrying": {
2639
+ const offline = entry?.status === "offline";
2640
+ this.raise({
2641
+ kind: "retrying",
2642
+ tone: "info",
2643
+ title: offline ? `${name2} is offline` : `${name2} didn't answer just now`,
2644
+ body: offline ? "Your request retries by itself. Your other AdaptiqAI models still work; pick one to continue now." : "Your request retries by itself.",
2645
+ actions: [],
2646
+ ...sessionId2 === void 0 ? {} : { sessionId: sessionId2 }
2647
+ });
2648
+ break;
2649
+ }
2428
2650
  case "offline":
2429
- this.raise({ kind: "offline", tone: "warning", title: "AdaptiqAI can't be reached", body: failure.message, actions: [] });
2651
+ this.raise({ kind: "offline", tone: "warning", title: "AdaptiqAI can't be reached", body: "Your request retries by itself once the connection is back.", actions: [], ...sessionId2 === void 0 ? {} : { sessionId: sessionId2 } });
2430
2652
  break;
2431
2653
  default:
2432
2654
  break;
@@ -2436,8 +2658,23 @@ var Workspace = class {
2436
2658
  if (sessionId2 === void 0)
2437
2659
  return;
2438
2660
  const session = this.session(sessionId2);
2439
- if (session.phase !== "interrupted")
2440
- this.setPhase(session, "idle");
2661
+ session.inflight = Math.max(0, session.inflight - 1);
2662
+ if (session.failedNow && session.retry !== null) {
2663
+ const retry = session.retry;
2664
+ clearTimeout(session.waitTimer);
2665
+ session.waitTimer = setTimeout(() => {
2666
+ if (session.inflight === 0 && session.retry === retry) {
2667
+ session.retry = null;
2668
+ this.setPhase(session, "interrupted");
2669
+ }
2670
+ }, Math.max(0, retry.until - this.now()));
2671
+ session.waitTimer.unref?.();
2672
+ } else {
2673
+ if (!session.failedNow)
2674
+ session.retry = null;
2675
+ if (session.phase !== "interrupted")
2676
+ this.setPhase(session, "idle");
2677
+ }
2441
2678
  if (session.listeners.size > 0) {
2442
2679
  setTimeout(() => {
2443
2680
  void this.loadSession(session);
@@ -1,18 +1,39 @@
1
- import type { AccountSnapshot } from '@adaptiqai/core/types';
1
+ import type { AccountPhase, AccountSnapshot } from '@adaptiqai/core/types';
2
2
  import type { Kit } from './actions.ts';
3
3
  import { type T } from './ui.tsx';
4
+ /** How long "Connected" stays before the card moves on by itself. */
5
+ export declare const CONNECTED_MOMENT_MS = 1500;
6
+ /**
7
+ * The moment right after the browser approval: true from the switch
8
+ * signing-in → connected until {@link CONNECTED_MOMENT_MS} later or dismiss.
9
+ * @param phase - the account phase, or null before it is known.
10
+ * @returns whether to show it, and how to end it early.
11
+ */
12
+ export declare function useConnectedMoment(phase: AccountPhase | null, ms?: number): {
13
+ show: boolean;
14
+ dismiss: () => void;
15
+ };
16
+ /** "Connected to <workspace>": the stones settle, a check lands. Clicking anywhere ends it. */
17
+ export declare function ConnectedMoment({ t, workspace, onClose }: {
18
+ t: T;
19
+ workspace: string | null;
20
+ onClose: () => void;
21
+ }): import("react").JSX.Element;
4
22
  /** The code split in two halves of boxed characters. */
5
23
  export declare function DeviceCode({ code }: {
6
24
  code: string;
7
25
  }): import("react").JSX.Element;
8
26
  /**
9
27
  * Hero content for each sign-in state. Lives on a navy hero surface.
10
- * @param props - account snapshot, kit, translate.
28
+ * @param props - account snapshot, kit, translate, and a quiet second action (Later, Cancel).
11
29
  * @returns the hero body.
12
30
  */
13
- export declare function ConnectHero({ account, kit, t, compact }: {
31
+ export declare function ConnectHero({ account, kit, t, aside }: {
14
32
  account: AccountSnapshot | null;
15
33
  kit: Kit;
16
34
  t: T;
17
- compact?: boolean;
35
+ aside?: {
36
+ label: string;
37
+ onClick: () => void;
38
+ };
18
39
  }): import("react").JSX.Element;
@@ -1,4 +1,4 @@
1
- import type { AccountSnapshot, ModelView } from '@adaptiqai/core/types';
1
+ import type { AccountSnapshot, MemoryView, ModelView } from '@adaptiqai/core/types';
2
2
  import type { Kit } from './actions.ts';
3
3
  import { type T } from './ui.tsx';
4
4
  type Hook<S> = <R>(select: (snapshot: S) => R) => R;
@@ -7,6 +7,15 @@ export interface SettingsProps {
7
7
  kit: Kit;
8
8
  useAqAccount: Hook<AccountSnapshot | null>;
9
9
  }
10
+ /** The fullness chip: green below 70 %, amber to 90 %, red above. */
11
+ export declare function MemoryChip({ store }: {
12
+ store: MemoryView;
13
+ }): import("react").JSX.Element;
14
+ /** The store's bar, in the same colour as its chip. */
15
+ export declare function MemoryBar({ store, thin }: {
16
+ store: MemoryView;
17
+ thin?: boolean;
18
+ }): import("react").JSX.Element;
10
19
  /**
11
20
  * A model's facts, the same fields in the same order for every model:
12
21
  * memory, thinking levels, region. A field is left out only when unknown.
@@ -0,0 +1,59 @@
1
+ /** The composer pill's words and timing, kept apart from React so they can be tested. */
2
+ import type { WaitKind } from '@adaptiqai/core/types';
3
+ type T = (key: string, params?: Record<string, unknown>) => string;
4
+ /** How long "+24.6k from memory" stays after a reply. */
5
+ export declare const AFTER_REPLY_MS = 4000;
6
+ /** How long the count-up to that figure takes. */
7
+ export declare const COUNT_UP_MS = 900;
8
+ /**
9
+ * The pill's words while a request waits.
10
+ * @param t - translate.
11
+ * @param kind - what the request waits for.
12
+ * @returns e.g. "Waiting for a free lane".
13
+ */
14
+ export declare function waitText(t: T, kind: WaitKind): string;
15
+ /**
16
+ * The pill's words after a reply, split around the figure so it can be set heavier.
17
+ * @param t - translate.
18
+ * @param fromMemory - tokens the reply read from memory (shown so far, while counting up).
19
+ * @returns e.g. { before: '', figure: '+24.6k', after: ' from memory' }.
20
+ */
21
+ export declare function afterReplyParts(t: T, fromMemory: number): {
22
+ before: string;
23
+ figure: string;
24
+ after: string;
25
+ };
26
+ /**
27
+ * The pill's words after a reply.
28
+ * @param t - translate.
29
+ * @param fromMemory - tokens the reply read from memory.
30
+ * @returns e.g. "+24.6k from memory".
31
+ */
32
+ export declare function afterReplyText(t: T, fromMemory: number): string;
33
+ /**
34
+ * The figure partway through the count-up: fast at first, settling softly.
35
+ * @param target - the final figure.
36
+ * @param elapsed - ms since the count-up began.
37
+ * @param duration - ms the count-up takes.
38
+ * @returns the figure to show now.
39
+ */
40
+ export declare function countUp(target: number, elapsed: number, duration?: number): number;
41
+ /**
42
+ * Follows a conversation's last turn and reports a new reply that read from
43
+ * memory for {@link AFTER_REPLY_MS}. The turn already there when the view
44
+ * first arrives is history, not news.
45
+ */
46
+ export declare class AfterReply {
47
+ private readonly onChange;
48
+ private readonly ms;
49
+ private baseline;
50
+ private timer;
51
+ constructor(onChange: (fromMemory: number | null) => void, ms?: number);
52
+ /** The conversation's view arrived or changed. */
53
+ seen(turn: {
54
+ readonly at: number;
55
+ readonly fromMemory: number;
56
+ } | null): void;
57
+ dispose(): void;
58
+ }
59
+ export {};