@adaptiqai/deepseek-harness 0.2.3 → 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;
@@ -1261,6 +1275,11 @@ var str4 = (value) => typeof value === "string" && value.trim() !== "" ? value.t
1261
1275
  var count2 = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
1262
1276
  var DEFAULT_PREFERENCES = { rememberByDefault: true, welcomed: false };
1263
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
+ }
1264
1283
  function copyKey(source, target) {
1265
1284
  return `${source}\0${target ?? ""}`;
1266
1285
  }
@@ -1436,6 +1455,10 @@ var Workspace = class {
1436
1455
  catalog = [];
1437
1456
  catalogKey = "";
1438
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;
1439
1462
  offers = [];
1440
1463
  checkoutMode = null;
1441
1464
  /** Offer id → what the account summary says about it (model id, console link). */
@@ -1655,7 +1678,8 @@ var Workspace = class {
1655
1678
  return;
1656
1679
  this.timer = setTimeout(() => {
1657
1680
  void this.refresh().finally(() => {
1658
- 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)));
1659
1683
  });
1660
1684
  }, ms);
1661
1685
  this.timer.unref?.();
@@ -1690,8 +1714,17 @@ var Workspace = class {
1690
1714
  this.setCatalog([]);
1691
1715
  return;
1692
1716
  }
1693
- const rows = await this.deps.api.json("/v1/models");
1694
- 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
+ }
1695
1728
  const summary2 = await this.loadSummary();
1696
1729
  let availability;
1697
1730
  if (summary2 !== null) {
@@ -1708,19 +1741,33 @@ var Workspace = class {
1708
1741
  if (this.memory === null || !this.memory.available)
1709
1742
  await this.loadFallbackMemory();
1710
1743
  this.lastError = null;
1744
+ this.trouble = null;
1711
1745
  this.refreshedAt = this.now();
1712
1746
  this.evaluateNotices();
1713
1747
  } catch (error) {
1714
1748
  const failure = error instanceof AdaptiqHttpError ? error.failure : null;
1715
1749
  this.lastError = { code: failure?.code ?? "UNKNOWN", message: failure?.message ?? (error instanceof Error ? error.message : String(error)), at: this.now() };
1716
- if (failure?.notice === "offline")
1717
- 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
+ }
1718
1757
  this.deps.log?.("warn", `adaptiq: refresh failed: ${this.lastError.message}`);
1719
1758
  } finally {
1720
1759
  this.loading = false;
1721
1760
  this.publish();
1722
1761
  }
1723
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
+ }
1724
1771
  setCatalog(models) {
1725
1772
  const key = JSON.stringify(models);
1726
1773
  this.catalogAt = this.now();
@@ -1733,8 +1780,13 @@ var Workspace = class {
1733
1780
  if (session.listeners.size === 0)
1734
1781
  continue;
1735
1782
  this.emitSession(session);
1736
- 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) {
1737
1788
  this.pollSession(session, 0);
1789
+ }
1738
1790
  }
1739
1791
  }
1740
1792
  async loadSummary() {
@@ -2085,6 +2137,9 @@ var Workspace = class {
2085
2137
  knowledge: [],
2086
2138
  conversations: [],
2087
2139
  remoteAt: 0,
2140
+ home: null,
2141
+ notOn: /* @__PURE__ */ new Set(),
2142
+ backoff: null,
2088
2143
  listeners: /* @__PURE__ */ new Set()
2089
2144
  };
2090
2145
  this.sessions.set(id, session);
@@ -2162,13 +2217,29 @@ var Workspace = class {
2162
2217
  clearTimeout(session.timer);
2163
2218
  if (this.disposed || session.listeners.size === 0)
2164
2219
  return;
2220
+ const wait = session.backoff === null ? 0 : session.backoff.until - this.now();
2165
2221
  session.timer = setTimeout(() => {
2166
2222
  void this.loadSession(session).finally(() => {
2167
2223
  this.pollSession(session, sessionPollDelay(session));
2168
2224
  });
2169
- }, ms);
2225
+ }, Math.max(ms, wait));
2170
2226
  session.timer.unref?.();
2171
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
+ }
2172
2243
  /**
2173
2244
  * The memory store a conversation's knowledge and memory live in: where it is
2174
2245
  * stored once it exists, otherwise the model selected for it.
@@ -2255,42 +2326,23 @@ var Workspace = class {
2255
2326
  return;
2256
2327
  session.model = route.model;
2257
2328
  }
2258
- const path = `/v1/sessions/${encodeURIComponent(session.id)}`;
2259
- let body = null;
2260
- let routed = false;
2261
- let suggested = [];
2262
- try {
2263
- body = await this.deps.api.json(path, { allow: [404] });
2264
- routed = true;
2265
- } catch (error) {
2266
- if (!(error instanceof AdaptiqHttpError) || error.status !== 409 && error.status !== 400)
2267
- throw error;
2268
- const models = obj4(obj4(error.body)?.detail)?.models;
2269
- if (Array.isArray(models))
2270
- suggested = models.filter((model2) => typeof model2 === "string" && model2 !== "");
2271
- }
2272
- 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);
2273
- for (const model2 of candidates) {
2274
- body = await this.deps.api.json(path, { headers: { "x-adaptiq-model": model2 }, allow: [404] });
2275
- if (body !== null) {
2276
- if (session.model === null) {
2277
- session.model = model2;
2278
- session.onAdaptiq ??= true;
2279
- }
2280
- break;
2281
- }
2282
- }
2283
- if (routed && body !== null) {
2284
- const served = str4(obj4(body.session)?.model) ?? str4(body.model);
2285
- if (served !== null)
2286
- session.model = served;
2287
- else if (session.model === null && session.lastUsed !== null)
2288
- session.model = session.lastUsed;
2289
- session.onAdaptiq ??= true;
2329
+ const found = await this.findSession(session);
2330
+ if (found === void 0) {
2331
+ session.remoteAt = this.now();
2332
+ return;
2290
2333
  }
2291
- if (routed || candidates.length > 0)
2334
+ session.backoff = null;
2335
+ if (found.asked)
2292
2336
  session.remoteAt = this.now();
2337
+ const body = found.body;
2293
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
+ }
2294
2346
  session.exists = false;
2295
2347
  session.knowledge = [];
2296
2348
  return;
@@ -2314,6 +2366,7 @@ var Workspace = class {
2314
2366
  session.conversations = await this.attachedConversations(session).catch(() => session.conversations);
2315
2367
  } catch (error) {
2316
2368
  session.remoteAt = this.now();
2369
+ this.backOff(session, null);
2317
2370
  this.deps.log?.("warn", `adaptiq: could not read conversation ${session.id}: ${error instanceof Error ? error.message : String(error)}`);
2318
2371
  if (!(error instanceof AdaptiqHttpError))
2319
2372
  throw error;
@@ -2329,6 +2382,83 @@ var Workspace = class {
2329
2382
  session.loading = void 0;
2330
2383
  }
2331
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
+ }
2332
2462
  /**
2333
2463
  * Choose whether new messages of a conversation are saved.
2334
2464
  * @param id - harness session id.
@@ -2387,6 +2517,8 @@ var Workspace = class {
2387
2517
  const session = this.session(sessionId2);
2388
2518
  session.onAdaptiq = true;
2389
2519
  session.model = model2;
2520
+ session.notOn.delete(model2);
2521
+ session.backoff = null;
2390
2522
  session.inflight++;
2391
2523
  session.laneHeld = false;
2392
2524
  session.failedNow = false;
@@ -2420,7 +2552,7 @@ var Workspace = class {
2420
2552
  session.laneHeld = true;
2421
2553
  session.retry = null;
2422
2554
  this.setPhase(session, "responding");
2423
- this.clearNotices("busy", "preparing", "offline");
2555
+ this.clearNotices("busy", "preparing", "offline", "retrying");
2424
2556
  }
2425
2557
  usage(sessionId2, model2, usage2) {
2426
2558
  if (sessionId2 === void 0)
@@ -2503,8 +2635,20 @@ var Workspace = class {
2503
2635
  case "model-unavailable":
2504
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 }] });
2505
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
+ }
2506
2650
  case "offline":
2507
- 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 } });
2508
2652
  break;
2509
2653
  default:
2510
2654
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adaptiqai/deepseek-harness",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "AdaptiqAI for DeepSeek Harness: models with 5M-token native memory and Knowledge. Sign up, get a model and work without leaving the harness",
5
5
  "keywords": [
6
6
  "dsh-plugin",
@@ -89,7 +89,7 @@
89
89
  "react-dom": "~18.3.1",
90
90
  "typescript": "~5.9.3",
91
91
  "vitest": "~3.2.7",
92
- "@adaptiqai/core": "0.2.3"
92
+ "@adaptiqai/core": "0.2.4"
93
93
  },
94
94
  "repository": {
95
95
  "type": "git",