@adaptiqai/deepseek-harness 0.2.3 → 0.2.5

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,27 @@ 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 ATTACH_POLL_MS = 15e3;
1279
+ var ATTACH_BACKOFF_MAX_MS = 10 * 6e4;
1280
+ var ATTACH_DEADLINE_MS = 6 * 36e5;
1281
+ var MODEL_ROUTE_CODES = /* @__PURE__ */ new Set(["memory_route_required", "model_required", "model_mismatch"]);
1282
+ function attachBackoffMs(failures) {
1283
+ return Math.min(ATTACH_BACKOFF_MAX_MS, ATTACH_POLL_MS * 2 ** Math.min(Math.max(failures - 1, 0), 10));
1284
+ }
1285
+ function routeChoices(detail) {
1286
+ if (!Array.isArray(detail.models))
1287
+ return null;
1288
+ return detail.models.flatMap((raw) => {
1289
+ const entry = obj4(raw);
1290
+ const id = typeof raw === "string" ? raw : str4(entry?.id) ?? str4(entry?.model);
1291
+ return id === null || id === "" ? [] : [id];
1292
+ });
1293
+ }
1294
+ var TROUBLE_QUIET_MS = 6e4;
1295
+ var SESSION_BACKOFF_MAX_MS = 5 * 6e4;
1296
+ function sessionBackoffMs(failures) {
1297
+ return Math.min(SESSION_BACKOFF_MAX_MS, 3e4 * 2 ** Math.max(1, failures));
1298
+ }
1264
1299
  function copyKey(source, target) {
1265
1300
  return `${source}\0${target ?? ""}`;
1266
1301
  }
@@ -1436,6 +1471,10 @@ var Workspace = class {
1436
1471
  catalog = [];
1437
1472
  catalogKey = "";
1438
1473
  catalogAt = 0;
1474
+ /** The last `/v1/models` rows read: one stalled engine can fail the list for a while. */
1475
+ modelRows = [];
1476
+ /** Background reads have failed for a passing reason since `since`. */
1477
+ trouble = null;
1439
1478
  offers = [];
1440
1479
  checkoutMode = null;
1441
1480
  /** Offer id → what the account summary says about it (model id, console link). */
@@ -1655,7 +1694,8 @@ var Workspace = class {
1655
1694
  return;
1656
1695
  this.timer = setTimeout(() => {
1657
1696
  void this.refresh().finally(() => {
1658
- this.schedule(this.listeners.size > 0 ? this.deps.watchedRefreshMs : this.deps.refreshMs);
1697
+ const normal = this.listeners.size > 0 ? this.deps.watchedRefreshMs : this.deps.refreshMs;
1698
+ this.schedule(this.trouble === null ? normal : Math.min(normal, 15e3 * 2 ** Math.min(this.trouble.failures - 1, 6)));
1659
1699
  });
1660
1700
  }, ms);
1661
1701
  this.timer.unref?.();
@@ -1690,8 +1730,17 @@ var Workspace = class {
1690
1730
  this.setCatalog([]);
1691
1731
  return;
1692
1732
  }
1693
- const rows = await this.deps.api.json("/v1/models");
1694
- const data = Array.isArray(rows?.data) ? rows.data : [];
1733
+ let data;
1734
+ try {
1735
+ const rows = await this.deps.api.json("/v1/models");
1736
+ data = Array.isArray(rows?.data) ? rows.data : [];
1737
+ this.modelRows = data;
1738
+ } catch (error) {
1739
+ if (errorReach(error) === null || this.modelRows.length === 0)
1740
+ throw error;
1741
+ this.deps.log?.("warn", `adaptiq: model list unavailable, keeping the last one: ${error instanceof Error ? error.message : String(error)}`);
1742
+ data = this.modelRows;
1743
+ }
1695
1744
  const summary2 = await this.loadSummary();
1696
1745
  let availability;
1697
1746
  if (summary2 !== null) {
@@ -1708,19 +1757,33 @@ var Workspace = class {
1708
1757
  if (this.memory === null || !this.memory.available)
1709
1758
  await this.loadFallbackMemory();
1710
1759
  this.lastError = null;
1760
+ this.trouble = null;
1711
1761
  this.refreshedAt = this.now();
1712
1762
  this.evaluateNotices();
1713
1763
  } catch (error) {
1714
1764
  const failure = error instanceof AdaptiqHttpError ? error.failure : null;
1715
1765
  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" }] });
1766
+ const reach = failure === null ? null : failureReach(failure);
1767
+ if (reach !== null) {
1768
+ const since = this.trouble?.since ?? this.now();
1769
+ this.trouble = { since, reach, failures: (this.trouble?.failures ?? 0) + 1 };
1770
+ if (this.now() - since >= TROUBLE_QUIET_MS)
1771
+ this.raiseTrouble(reach);
1772
+ }
1718
1773
  this.deps.log?.("warn", `adaptiq: refresh failed: ${this.lastError.message}`);
1719
1774
  } finally {
1720
1775
  this.loading = false;
1721
1776
  this.publish();
1722
1777
  }
1723
1778
  }
1779
+ /**
1780
+ * Say, calmly, that background reads keep failing: "busy" while AdaptiqAI
1781
+ * answers with errors, "can't be reached" only when there is no connection.
1782
+ * @param reach - the kind of trouble.
1783
+ */
1784
+ raiseTrouble(reach) {
1785
+ 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: [] });
1786
+ }
1724
1787
  setCatalog(models) {
1725
1788
  const key = JSON.stringify(models);
1726
1789
  this.catalogAt = this.now();
@@ -1733,8 +1796,13 @@ var Workspace = class {
1733
1796
  if (session.listeners.size === 0)
1734
1797
  continue;
1735
1798
  this.emitSession(session);
1736
- if (!session.exists)
1799
+ const awaited = session.backoff?.model ?? null;
1800
+ if (awaited !== null && !this.modelDown(awaited)) {
1801
+ session.backoff = null;
1737
1802
  this.pollSession(session, 0);
1803
+ } else if (!session.exists && session.remoteAt === 0) {
1804
+ this.pollSession(session, 0);
1805
+ }
1738
1806
  }
1739
1807
  }
1740
1808
  async loadSummary() {
@@ -2085,6 +2153,9 @@ var Workspace = class {
2085
2153
  knowledge: [],
2086
2154
  conversations: [],
2087
2155
  remoteAt: 0,
2156
+ home: null,
2157
+ notOn: /* @__PURE__ */ new Set(),
2158
+ backoff: null,
2088
2159
  listeners: /* @__PURE__ */ new Set()
2089
2160
  };
2090
2161
  this.sessions.set(id, session);
@@ -2162,13 +2233,29 @@ var Workspace = class {
2162
2233
  clearTimeout(session.timer);
2163
2234
  if (this.disposed || session.listeners.size === 0)
2164
2235
  return;
2236
+ const wait = session.backoff === null ? 0 : session.backoff.until - this.now();
2165
2237
  session.timer = setTimeout(() => {
2166
2238
  void this.loadSession(session).finally(() => {
2167
2239
  this.pollSession(session, sessionPollDelay(session));
2168
2240
  });
2169
- }, ms);
2241
+ }, Math.max(ms, wait));
2170
2242
  session.timer.unref?.();
2171
2243
  }
2244
+ /** @returns whether a model's instance is offline or disabled (not asked about conversations). */
2245
+ modelDown(id) {
2246
+ const status = this.catalog.find((entry) => entry.id === id)?.status;
2247
+ return status === "offline" || status === "disabled";
2248
+ }
2249
+ /**
2250
+ * Read a conversation less often after a failed read or while its model is
2251
+ * offline: 1, 2, 4, then every 5 minutes.
2252
+ * @param session - the conversation.
2253
+ * @param model - the offline model it waits for, or null after a failed read.
2254
+ */
2255
+ backOff(session, model2) {
2256
+ const failures = (session.backoff?.failures ?? 0) + 1;
2257
+ session.backoff = { failures, until: this.now() + sessionBackoffMs(failures), model: model2 };
2258
+ }
2172
2259
  /**
2173
2260
  * The memory store a conversation's knowledge and memory live in: where it is
2174
2261
  * stored once it exists, otherwise the model selected for it.
@@ -2255,42 +2342,23 @@ var Workspace = class {
2255
2342
  return;
2256
2343
  session.model = route.model;
2257
2344
  }
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;
2345
+ const found = await this.findSession(session);
2346
+ if (found === void 0) {
2347
+ session.remoteAt = this.now();
2348
+ return;
2290
2349
  }
2291
- if (routed || candidates.length > 0)
2350
+ session.backoff = null;
2351
+ if (found.asked)
2292
2352
  session.remoteAt = this.now();
2353
+ const body = found.body;
2293
2354
  if (body === null) {
2355
+ if (session.exists) {
2356
+ session.memoryTokens = null;
2357
+ session.saved = null;
2358
+ session.optimizing = false;
2359
+ session.revision = null;
2360
+ session.conversations = [];
2361
+ }
2294
2362
  session.exists = false;
2295
2363
  session.knowledge = [];
2296
2364
  return;
@@ -2314,6 +2382,7 @@ var Workspace = class {
2314
2382
  session.conversations = await this.attachedConversations(session).catch(() => session.conversations);
2315
2383
  } catch (error) {
2316
2384
  session.remoteAt = this.now();
2385
+ this.backOff(session, null);
2317
2386
  this.deps.log?.("warn", `adaptiq: could not read conversation ${session.id}: ${error instanceof Error ? error.message : String(error)}`);
2318
2387
  if (!(error instanceof AdaptiqHttpError))
2319
2388
  throw error;
@@ -2329,6 +2398,83 @@ var Workspace = class {
2329
2398
  session.loading = void 0;
2330
2399
  }
2331
2400
  }
2401
+ /**
2402
+ * Find a conversation on the server. Once a model has answered for it, only
2403
+ * that model is asked. Before that, the session-aware route (which needs no
2404
+ * model), then, on gateways that want one, the conversation's model or the
2405
+ * models the gateway names: never a model that is offline or that already
2406
+ * said the conversation is not there.
2407
+ * @param session - the conversation.
2408
+ * @returns its body (null when the server does not have it) and whether
2409
+ * anything could be asked, or undefined when its model is offline (backed off).
2410
+ */
2411
+ async findSession(session) {
2412
+ const path = `/v1/sessions/${encodeURIComponent(session.id)}`;
2413
+ const ask = async (model2) => {
2414
+ try {
2415
+ return await this.deps.api.json(path, { headers: { "x-adaptiq-model": model2 }, allow: [404] });
2416
+ } catch (error) {
2417
+ if (error instanceof AdaptiqHttpError && error.status === 409)
2418
+ return null;
2419
+ throw error;
2420
+ }
2421
+ };
2422
+ if (session.home !== null) {
2423
+ const home = session.home;
2424
+ if (this.modelDown(home)) {
2425
+ this.backOff(session, home);
2426
+ return void 0;
2427
+ }
2428
+ const body = await ask(home);
2429
+ if (body === null) {
2430
+ session.home = null;
2431
+ session.notOn.add(home);
2432
+ } else
2433
+ session.model = home;
2434
+ return { body, asked: true };
2435
+ }
2436
+ let suggested = [];
2437
+ try {
2438
+ const body = await this.deps.api.json(path, { allow: [404] });
2439
+ if (body !== null) {
2440
+ const served = str4(obj4(body.session)?.model) ?? str4(body.model);
2441
+ if (served !== null) {
2442
+ session.model = served;
2443
+ session.home = served;
2444
+ } else if (session.model === null && session.lastUsed !== null)
2445
+ session.model = session.lastUsed;
2446
+ session.onAdaptiq ??= true;
2447
+ }
2448
+ return { body, asked: true };
2449
+ } catch (error) {
2450
+ if (!(error instanceof AdaptiqHttpError) || error.status !== 409 && error.status !== 400)
2451
+ throw error;
2452
+ const models = obj4(obj4(error.body)?.detail)?.models;
2453
+ if (Array.isArray(models))
2454
+ suggested = models.filter((model2) => typeof model2 === "string" && model2 !== "");
2455
+ }
2456
+ 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);
2457
+ const open = listed.filter((model2) => !session.notOn.has(model2)).slice(0, 6);
2458
+ const candidates = open.filter((model2) => !this.modelDown(model2));
2459
+ for (const model2 of candidates) {
2460
+ const body = await ask(model2);
2461
+ if (body !== null) {
2462
+ session.home = model2;
2463
+ if (session.model === null) {
2464
+ session.model = model2;
2465
+ session.onAdaptiq ??= true;
2466
+ }
2467
+ return { body, asked: true };
2468
+ }
2469
+ session.notOn.add(model2);
2470
+ }
2471
+ const offline = open.find((model2) => this.modelDown(model2));
2472
+ if (offline !== void 0) {
2473
+ this.backOff(session, offline);
2474
+ return void 0;
2475
+ }
2476
+ return { body: null, asked: candidates.length > 0 };
2477
+ }
2332
2478
  /**
2333
2479
  * Choose whether new messages of a conversation are saved.
2334
2480
  * @param id - harness session id.
@@ -2387,6 +2533,8 @@ var Workspace = class {
2387
2533
  const session = this.session(sessionId2);
2388
2534
  session.onAdaptiq = true;
2389
2535
  session.model = model2;
2536
+ session.notOn.delete(model2);
2537
+ session.backoff = null;
2390
2538
  session.inflight++;
2391
2539
  session.laneHeld = false;
2392
2540
  session.failedNow = false;
@@ -2420,7 +2568,7 @@ var Workspace = class {
2420
2568
  session.laneHeld = true;
2421
2569
  session.retry = null;
2422
2570
  this.setPhase(session, "responding");
2423
- this.clearNotices("busy", "preparing", "offline");
2571
+ this.clearNotices("busy", "preparing", "offline", "retrying");
2424
2572
  }
2425
2573
  usage(sessionId2, model2, usage2) {
2426
2574
  if (sessionId2 === void 0)
@@ -2503,8 +2651,20 @@ var Workspace = class {
2503
2651
  case "model-unavailable":
2504
2652
  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
2653
  break;
2654
+ case "retrying": {
2655
+ const offline = entry?.status === "offline";
2656
+ this.raise({
2657
+ kind: "retrying",
2658
+ tone: "info",
2659
+ title: offline ? `${name2} is offline` : `${name2} didn't answer just now`,
2660
+ body: offline ? "Your request retries by itself. Your other AdaptiqAI models still work; pick one to continue now." : "Your request retries by itself.",
2661
+ actions: [],
2662
+ ...sessionId2 === void 0 ? {} : { sessionId: sessionId2 }
2663
+ });
2664
+ break;
2665
+ }
2506
2666
  case "offline":
2507
- this.raise({ kind: "offline", tone: "warning", title: "AdaptiqAI can't be reached", body: failure.message, actions: [] });
2667
+ 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
2668
  break;
2509
2669
  default:
2510
2670
  break;
@@ -2555,7 +2715,10 @@ var Workspace = class {
2555
2715
  if (session !== void 0 && this.now() - session.remoteAt > 1e4)
2556
2716
  await this.loadSession(session).catch(() => {
2557
2717
  });
2558
- const target = model2 !== void 0 && this.catalog.some((entry) => entry.id === model2) ? model2 : this.targetModel(session) ?? null;
2718
+ const named = model2 !== void 0 && model2 !== "" && (this.catalog.length === 0 || this.catalog.some((entry) => entry.id === model2));
2719
+ const target = named ? model2 : this.targetModel(session) ?? null;
2720
+ if (target === null)
2721
+ return { model: null, modelName: null, items: [], nextCursor: null, hasMore: false, summary: null };
2559
2722
  const attached = new Set(session?.knowledge.map((item) => item.id) ?? []);
2560
2723
  const name2 = (id) => this.modelName(id);
2561
2724
  const read = async (store, limit, after) => {
@@ -2564,7 +2727,7 @@ var Workspace = class {
2564
2727
  params.set("query", query.trim().slice(0, 200));
2565
2728
  if (after !== void 0 && after !== "")
2566
2729
  params.set("cursor", after);
2567
- const body = await this.deps.api.json(`/v1/memories?${params}`, { headers: store === null ? {} : { "x-adaptiq-model": store } });
2730
+ const body = await this.deps.api.json(`/v1/memories?${params}`, { headers: { "x-adaptiq-model": store } });
2568
2731
  const items = (Array.isArray(body?.data) ? body.data : []).flatMap((raw) => {
2569
2732
  const memory = obj4(raw);
2570
2733
  return memory === null ? [] : [knowledgeItem(memory, store === target && attached.has(String(memory.memory_id)), { store, target, name: name2 })];
@@ -2574,13 +2737,13 @@ var Workspace = class {
2574
2737
  const here = await read(target, PAGE, cursor);
2575
2738
  await this.followCopies(target);
2576
2739
  let elsewhere = [];
2577
- if ((cursor === void 0 || cursor === "") && target !== null) {
2740
+ if (cursor === void 0 || cursor === "") {
2578
2741
  const others = this.catalog.filter((entry) => entry.id !== target && entry.status !== "disabled").slice(0, 3);
2579
2742
  const pages = await Promise.all(others.map((entry) => read(entry.id, 20).catch(() => ({ items: [], pagination: null }))));
2580
2743
  const seen = new Set(here.items.map((item) => item.id));
2581
2744
  const copied = new Set([...this.copies.values()].filter((copy) => copy.target === target && copy.status === "ready").map((copy) => copy.source));
2582
2745
  const names = new Set(here.items.filter((item) => item.fits).map((item) => item.name));
2583
- const copyable = target !== null && this.catalog.some((entry) => entry.id === target);
2746
+ const copyable = this.catalog.some((entry) => entry.id === target);
2584
2747
  elsewhere = pages.flatMap((page) => page.items).filter((item) => !seen.has(item.id) && !copied.has(item.id) && !names.has(item.name)).map((item) => {
2585
2748
  const copy = this.copies.get(copyKey(item.id, target));
2586
2749
  return { ...item, fits: false, attached: false, convertible: copyable && item.kind === "collection", copying: copy === void 0 || copy.status === "ready" ? null : copy.status };
@@ -2591,7 +2754,7 @@ var Workspace = class {
2591
2754
  const misfits = here.items.filter((item) => !item.fits);
2592
2755
  return {
2593
2756
  model: target,
2594
- modelName: target === null ? null : this.modelName(target),
2757
+ modelName: this.modelName(target),
2595
2758
  items: [...fitting, ...misfits, ...elsewhere],
2596
2759
  nextCursor: str4(here.pagination?.next_cursor),
2597
2760
  hasMore: here.pagination?.has_more === true,
@@ -2790,7 +2953,8 @@ var Workspace = class {
2790
2953
  await this.setKnowledge(input.attachToSession, id, true).catch(() => {
2791
2954
  });
2792
2955
  else {
2793
- this.pendingAttach.set(id, { sessionId: input.attachToSession, at: this.now() });
2956
+ const model2 = str4(memory.model) ?? this.memoryModel(session) ?? null;
2957
+ this.pendingAttach.set(id, { sessionId: input.attachToSession, at: this.now(), model: model2, failures: 0, next: this.now() });
2794
2958
  this.scheduleAttach(1e4);
2795
2959
  }
2796
2960
  }
@@ -2805,25 +2969,102 @@ var Workspace = class {
2805
2969
  }, ms);
2806
2970
  this.attachTimer.unref?.();
2807
2971
  }
2972
+ /**
2973
+ * Read the collections waiting to be attached, each in its own model's store,
2974
+ * and attach the ones that are ready.
2975
+ */
2808
2976
  async attachReady() {
2977
+ const groups = /* @__PURE__ */ new Map();
2978
+ for (const [id, pending] of [...this.pendingAttach]) {
2979
+ if (this.now() - pending.at > ATTACH_DEADLINE_MS) {
2980
+ this.pendingAttach.delete(id);
2981
+ continue;
2982
+ }
2983
+ if (pending.next > this.now())
2984
+ continue;
2985
+ const model2 = pending.model ?? this.targetModel(this.session(pending.sessionId));
2986
+ if (model2 === void 0)
2987
+ continue;
2988
+ groups.set(model2, [...groups.get(model2) ?? [], [id, pending]]);
2989
+ }
2990
+ for (const [model2, entries] of groups) {
2991
+ try {
2992
+ await this.attachCheck(model2, entries);
2993
+ } catch {
2994
+ }
2995
+ }
2996
+ if (this.pendingAttach.size === 0)
2997
+ return;
2998
+ const now = this.now();
2999
+ this.scheduleAttach(Math.min(...[...this.pendingAttach.values()].map((pending) => pending.next > now ? pending.next - now : ATTACH_POLL_MS)));
3000
+ }
3001
+ async attachCheck(model2, entries) {
3002
+ let items = null;
2809
3003
  try {
2810
- const page = await this.listKnowledge();
2811
- for (const [id, pending] of [...this.pendingAttach]) {
2812
- const item = page.items.find((candidate) => candidate.id === id);
2813
- if (this.now() - pending.at > 6 * 36e5 || item?.status === "failed") {
2814
- this.pendingAttach.delete(id);
2815
- continue;
2816
- }
2817
- if (item?.status === "ready") {
3004
+ items = await this.waitingKnowledge(model2);
3005
+ } catch (error) {
3006
+ const detail = error instanceof AdaptiqHttpError && error.status === 409 ? obj4(obj4(error.body)?.detail) : null;
3007
+ const code = str4(detail?.code);
3008
+ if (detail === null || code === null || !MODEL_ROUTE_CODES.has(code)) {
3009
+ for (const [, pending] of entries)
3010
+ pending.next = this.now() + ATTACH_POLL_MS;
3011
+ return;
3012
+ }
3013
+ const choices = routeChoices(detail);
3014
+ if (choices !== null && choices.length > 0 && !choices.includes(model2)) {
3015
+ const name2 = this.modelName(model2) ?? model2;
3016
+ for (const [id, pending] of entries) {
2818
3017
  this.pendingAttach.delete(id);
2819
- await this.setKnowledge(pending.sessionId, id, true).catch(() => {
3018
+ this.raise({
3019
+ kind: "model-unavailable",
3020
+ tone: "info",
3021
+ title: `${name2} isn't available`,
3022
+ body: `Your new collection is saved. Add it from Knowledge once ${name2} is available again.`,
3023
+ actions: [{ id: "open-models", label: "See your models", url: this.links.models }],
3024
+ sessionId: pending.sessionId
2820
3025
  });
2821
- this.raise({ kind: "connected", tone: "success", title: `${item.name} is ready`, body: "It is now used in your conversation.", actions: [], sessionId: pending.sessionId });
2822
3026
  }
3027
+ return;
3028
+ }
3029
+ const pick = choices?.find((choice) => choice === model2);
3030
+ if (pick !== void 0)
3031
+ items = await this.waitingKnowledge(pick).catch(() => null);
3032
+ if (items === null) {
3033
+ for (const [, pending] of entries) {
3034
+ pending.failures += 1;
3035
+ pending.next = this.now() + attachBackoffMs(pending.failures);
3036
+ }
3037
+ return;
2823
3038
  }
2824
- } catch {
2825
3039
  }
2826
- this.scheduleAttach(15e3);
3040
+ for (const [id, pending] of entries) {
3041
+ const item = items.find((candidate) => candidate.id === id);
3042
+ pending.failures = 0;
3043
+ pending.next = this.now() + ATTACH_POLL_MS;
3044
+ if (item?.status === "failed") {
3045
+ this.pendingAttach.delete(id);
3046
+ continue;
3047
+ }
3048
+ if (item?.status === "ready") {
3049
+ this.pendingAttach.delete(id);
3050
+ await this.setKnowledge(pending.sessionId, id, true).catch(() => {
3051
+ });
3052
+ this.raise({ kind: "connected", tone: "success", title: `${item.name} is ready`, body: "It is now used in your conversation.", actions: [], sessionId: pending.sessionId });
3053
+ }
3054
+ }
3055
+ }
3056
+ /**
3057
+ * One model's collections, for the ones waiting to be attached.
3058
+ * @param model - the store to read.
3059
+ * @returns the collections.
3060
+ */
3061
+ async waitingKnowledge(model2) {
3062
+ const params = new URLSearchParams({ limit: String(PAGE), status: "all" });
3063
+ const body = await this.deps.api.json(`/v1/memories?${params}`, { headers: { "x-adaptiq-model": model2 } });
3064
+ return (Array.isArray(body?.data) ? body.data : []).flatMap((raw) => {
3065
+ const memory = obj4(raw);
3066
+ return memory === null ? [] : [knowledgeItem(memory, false)];
3067
+ });
2827
3068
  }
2828
3069
  // ---------------------------------------------------------------- conversations as knowledge
2829
3070
  /** False once the server answered that it has no conversation routes. */
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.5",
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.5"
93
93
  },
94
94
  "repository": {
95
95
  "type": "git",