dsh-modellix 0.2.1 → 0.3.0

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.d.ts CHANGED
@@ -7,7 +7,6 @@ import { z as z$1 } from "zod";
7
7
  import { ToolDefinition } from "@deepseek-ai/dsh-tools";
8
8
  import { Context } from "@deepseek-ai/cordis";
9
9
  import { Agent } from "@deepseek-ai/dsh-agent";
10
- import { ImageMediaType } from "@deepseek-ai/dsh-attachment";
11
10
 
12
11
  //#region src/core/config.d.ts
13
12
  declare const CURRENT_CONFIG_SCHEMA_VERSION: 1;
@@ -530,7 +529,7 @@ declare function extractAllowedSubmitUrl(document: Readonly<Record<string, unkno
530
529
  interface PreparedMediaUpload {
531
530
  readonly bytes: Uint8Array;
532
531
  readonly filename: string;
533
- readonly mimeType: ImageMediaType;
532
+ readonly mimeType: string;
534
533
  }
535
534
  interface MediaUploadRecord {
536
535
  readonly fileId: string;
@@ -721,6 +720,7 @@ interface ReadPredictionInput {
721
720
  readonly apiKey: string;
722
721
  readonly maxAttempts?: number;
723
722
  readonly signal?: AbortSignal;
723
+ readonly waitSeconds?: number;
724
724
  }
725
725
  declare class PredictionClient {
726
726
  #private;
@@ -729,6 +729,10 @@ declare class PredictionClient {
729
729
  submit(input: SubmitPredictionInput): Promise<PredictionTask>;
730
730
  /** GET retries only transient read failures and is capped at five attempts. */
731
731
  readTask(input: ReadPredictionInput): Promise<PredictionTask>;
732
+ /** Wait for one bounded server observation; does not submit or sleep between snapshots. */
733
+ waitTask(input: Omit<ReadPredictionInput, "waitSeconds"> & {
734
+ readonly timeoutSeconds?: number;
735
+ }): Promise<PredictionTask>;
732
736
  }
733
737
  declare function validateSubmitEndpoint(endpoint: string, modelSlug: string): URL;
734
738
  declare function parsePredictionTask(payload: unknown, expectedTaskId?: string): PredictionTask | null;
@@ -897,6 +901,7 @@ interface CredentialSnapshot {
897
901
  readonly credentialEpoch: number;
898
902
  }
899
903
  interface DesignHostControllerOptions {
904
+ readonly serializeObservation?: (operation: () => Promise<void>) => Promise<void>;
900
905
  readonly storage: StoragePort;
901
906
  readonly resolveCredential: () => Promise<CredentialSnapshot | undefined>;
902
907
  readonly isCredentialEpochCurrent: (credentialEpoch: number) => boolean;
@@ -988,6 +993,7 @@ declare class DesignHostController {
988
993
  constructor(options: DesignHostControllerOptions);
989
994
  handle(endpoint: string, payload: unknown, signal?: AbortSignal): Promise<DesignSnapshotWire>;
990
995
  pollRunning(signal?: AbortSignal): Promise<boolean>;
996
+ invalidateWaits(): void;
991
997
  private read;
992
998
  private refresh;
993
999
  private selectModel;
package/lib/index.js CHANGED
@@ -4,7 +4,7 @@ import { getOrCreateAnonymousUserId } from "@deepseek-ai/dsh-anonymous-user-id";
4
4
  import { credentialRef } from "@deepseek-ai/dsh-credentials";
5
5
  import { SettingsConflictError } from "@deepseek-ai/dsh-settings";
6
6
  import { lstat, open, realpath } from "node:fs/promises";
7
- import { basename, isAbsolute, relative, resolve } from "node:path";
7
+ import { basename, extname, isAbsolute, relative, resolve } from "node:path";
8
8
  import { WebError } from "@deepseek-ai/dsh-web";
9
9
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
10
10
  import { defineDomain } from "@deepseek-ai/dsh-storage-domain";
@@ -1547,6 +1547,10 @@ var MediaUploadClient = class {
1547
1547
  throw new DesignError("SUBMIT_UNKNOWN", "The media upload outcome is unknown; do not retry automatically", { status: response.status });
1548
1548
  }
1549
1549
  if (!response.ok) {
1550
+ if (response.status === 429) {
1551
+ response.body?.cancel().catch(() => void 0);
1552
+ throw new DesignError("SUBMIT_REJECTED", "Upload quota exceeded. List existing files and explicitly delete only unneeded files before trying again; no automatic cleanup was performed", { status: 429 });
1553
+ }
1550
1554
  const ambiguous = response.status === 408 || response.status === 409 || response.status === 425 || response.status === 429 || response.status >= 500;
1551
1555
  response.body?.cancel().catch(() => void 0);
1552
1556
  throw new DesignError(ambiguous ? "SUBMIT_UNKNOWN" : "SUBMIT_REJECTED", ambiguous ? "The media upload outcome is unknown; do not retry automatically" : `The media upload was rejected with HTTP ${String(response.status)}`, { status: response.status });
@@ -1591,12 +1595,19 @@ async function prepareMediaUploadFromPath(cwd, inputPath) {
1591
1595
  try {
1592
1596
  const opened = await handle.stat();
1593
1597
  if (!opened.isFile() || opened.size !== before.size || opened.dev !== before.dev || opened.ino !== before.ino) throw new DesignError("INVALID_ARGUMENT", "Media file changed while it was being opened");
1594
- bytes = await handle.readFile();
1598
+ const bounded = Buffer.alloc(before.size + 1);
1599
+ let offset = 0;
1600
+ while (offset < bounded.length) {
1601
+ const { bytesRead } = await handle.read(bounded, offset, bounded.length - offset, null);
1602
+ if (bytesRead === 0) break;
1603
+ offset += bytesRead;
1604
+ }
1605
+ bytes = bounded.subarray(0, offset);
1595
1606
  } finally {
1596
1607
  await handle.close();
1597
1608
  }
1598
1609
  if (bytes.byteLength !== before.size) throw new DesignError("INVALID_ARGUMENT", "Media file changed while it was being read");
1599
- const mimeType = detectImageMimeType(bytes);
1610
+ const mimeType = detectMediaMimeType(bytes, resolved);
1600
1611
  return {
1601
1612
  bytes,
1602
1613
  filename: safeFilename(basename(resolved), mimeType),
@@ -1611,7 +1622,74 @@ function within(root, candidate) {
1611
1622
  const suffix = relative(root, candidate);
1612
1623
  return suffix === "" || !suffix.startsWith("..") && !isAbsolute(suffix);
1613
1624
  }
1614
- function detectImageMimeType(bytes) {
1625
+ const MEDIA_EXTENSIONS = {
1626
+ jpg: "image/jpeg",
1627
+ jpeg: "image/jpeg",
1628
+ png: "image/png",
1629
+ webp: "image/webp",
1630
+ gif: "image/gif",
1631
+ bmp: "image/bmp",
1632
+ tif: "image/tiff",
1633
+ tiff: "image/tiff",
1634
+ heic: "image/heic",
1635
+ heif: "image/heif",
1636
+ mp4: "video/mp4",
1637
+ m4v: "video/x-m4v",
1638
+ webm: "video/webm",
1639
+ mov: "video/quicktime",
1640
+ mkv: "video/x-matroska",
1641
+ avi: "video/x-msvideo",
1642
+ mp3: "audio/mpeg",
1643
+ wav: "audio/wav",
1644
+ m4a: "audio/mp4",
1645
+ aac: "audio/aac",
1646
+ ogg: "audio/ogg",
1647
+ flac: "audio/flac",
1648
+ opus: "audio/opus",
1649
+ weba: "audio/webm"
1650
+ };
1651
+ function detectMediaMimeType(bytes, filename) {
1652
+ const extension = extname(filename).slice(1).toLowerCase();
1653
+ const expected = MEDIA_EXTENSIONS[extension];
1654
+ if (expected === void 0) throw new DesignError("INVALID_ARGUMENT", "Unsupported media file extension");
1655
+ const head = Buffer.from(bytes.subarray(0, 16)).toString("latin1");
1656
+ const riffType = head.startsWith("RIFF") ? head.slice(8, 12) : "";
1657
+ const iso = bytes.length >= 12 && head.slice(4, 8) === "ftyp";
1658
+ const ebml = head.startsWith("Eߣ");
1659
+ const audioFrame = bytes.length >= 2 && bytes[0] === 255 && ((bytes[1] ?? 0) & 224) === 224;
1660
+ const tiff = [
1661
+ 73,
1662
+ 73,
1663
+ 42,
1664
+ 0
1665
+ ].every((byte, index) => bytes[index] === byte) || [
1666
+ 77,
1667
+ 77,
1668
+ 0,
1669
+ 42
1670
+ ].every((byte, index) => bytes[index] === byte);
1671
+ if ({
1672
+ gif: /^GIF8[79]a/u.test(head),
1673
+ bmp: head.startsWith("BM"),
1674
+ tif: tiff,
1675
+ tiff,
1676
+ heic: iso && /^(?:heic|heix|hevc|hevx|mif1)$/u.test(head.slice(8, 12)),
1677
+ heif: iso && /^(?:heic|heix|hevc|hevx|mif1|msf1)$/u.test(head.slice(8, 12)),
1678
+ mp4: iso,
1679
+ m4v: iso,
1680
+ m4a: iso,
1681
+ mov: iso || head.slice(4, 8) === "moov",
1682
+ avi: riffType === "AVI ",
1683
+ wav: riffType === "WAVE",
1684
+ webm: ebml,
1685
+ mkv: ebml,
1686
+ weba: ebml,
1687
+ mp3: head.startsWith("ID3") || audioFrame,
1688
+ aac: audioFrame,
1689
+ ogg: head.startsWith("OggS"),
1690
+ opus: head.startsWith("OggS"),
1691
+ flac: head.startsWith("fLaC")
1692
+ }[extension] === true) return expected;
1615
1693
  if (bytes.length >= 8 && bytes.slice(0, 8).every((value, index) => value === [
1616
1694
  137,
1617
1695
  80,
@@ -1621,14 +1699,18 @@ function detectImageMimeType(bytes) {
1621
1699
  10,
1622
1700
  26,
1623
1701
  10
1624
- ][index])) return "image/png";
1625
- if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return "image/jpeg";
1702
+ ][index]) && expected === "image/png") return expected;
1703
+ if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) {
1704
+ if (expected === "image/jpeg") return expected;
1705
+ }
1626
1706
  const ascii = (start, end) => new TextDecoder("ascii").decode(bytes.slice(start, end));
1627
- if (bytes.length >= 12 && ascii(0, 4) === "RIFF" && ascii(8, 12) === "WEBP") return "image/webp";
1628
- throw new DesignError("INVALID_ARGUMENT", "Unsupported media type; use PNG, JPEG, or WebP");
1707
+ if (bytes.length >= 12 && ascii(0, 4) === "RIFF" && ascii(8, 12) === "WEBP") {
1708
+ if (expected === "image/webp") return expected;
1709
+ }
1710
+ throw new DesignError("INVALID_ARGUMENT", "Unsupported media type or content does not match its extension");
1629
1711
  }
1630
1712
  function safeFilename(value, mimeType) {
1631
- const extension = mimeType === "image/png" ? ".png" : mimeType === "image/webp" ? ".webp" : ".jpg";
1713
+ const extension = `.${Object.entries(MEDIA_EXTENSIONS).find(([, type]) => type === mimeType)?.[0] ?? "bin"}`;
1632
1714
  const headerSafe = value.normalize("NFC").replaceAll(/["\\/]/gu, "_");
1633
1715
  const cleaned = Array.from(headerSafe, (character) => {
1634
1716
  const codePoint = character.codePointAt(0) ?? 0;
@@ -2510,10 +2592,18 @@ var PredictionClient = class {
2510
2592
  const apiKey = validateApiKey(input.apiKey);
2511
2593
  const maxAttempts = boundedAttempts(input.maxAttempts ?? 3);
2512
2594
  const url = new URL(`/api/v1/tasks/${encodeURIComponent(taskId)}`, PREDICTION_ORIGIN);
2595
+ if (input.waitSeconds !== void 0) {
2596
+ if (!Number.isSafeInteger(input.waitSeconds) || input.waitSeconds < 1 || input.waitSeconds > 45) throw new DesignError("INVALID_ARGUMENT", "Wait duration must be an integer from 1 to 45 seconds");
2597
+ url.searchParams.set("wait", "true");
2598
+ url.searchParams.set("timeout", String(input.waitSeconds));
2599
+ }
2600
+ const totalTimeout = input.waitSeconds === void 0 ? this.#requestTimeoutMs : input.waitSeconds * 1e3 + 5e3;
2601
+ const overall = input.waitSeconds === void 0 ? void 0 : requestDeadline(input.signal, totalTimeout);
2513
2602
  let lastFailure = null;
2514
2603
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
2515
2604
  input.signal?.throwIfAborted();
2516
- const deadline = requestDeadline(input.signal, this.#requestTimeoutMs);
2605
+ overall?.signal.throwIfAborted();
2606
+ const deadline = overall ?? requestDeadline(input.signal, this.#requestTimeoutMs);
2517
2607
  try {
2518
2608
  const response = await this.#fetch(url, {
2519
2609
  method: "GET",
@@ -2531,7 +2621,7 @@ var PredictionClient = class {
2531
2621
  retryAfterMs: failure.retryAfterMs
2532
2622
  });
2533
2623
  lastFailure = failure;
2534
- await this.#waitBeforeRetry(attempt, failure.retryAfterMs, input.signal);
2624
+ await this.#waitBeforeRetry(attempt, failure.retryAfterMs, overall?.signal ?? input.signal);
2535
2625
  continue;
2536
2626
  }
2537
2627
  let payload;
@@ -2543,6 +2633,10 @@ var PredictionClient = class {
2543
2633
  }
2544
2634
  const task = parsePredictionTask(payload, taskId);
2545
2635
  if (task === null) throw new DesignError("UNEXPECTED_RESPONSE", "Task status response does not contain a valid task");
2636
+ if (input.waitSeconds !== void 0 && (task.status === "queued" || task.status === "running")) {
2637
+ const wait = asRecord$3(asRecord$3(asRecord$3(payload)?.data)?.wait);
2638
+ if (typeof wait?.timed_out !== "boolean" || typeof wait.elapsed_ms !== "number") throw new DesignError("UNEXPECTED_RESPONSE", "The task API does not support server-side wait; keep the task ID and do not resubmit");
2639
+ }
2546
2640
  this.#log({
2547
2641
  level: "info",
2548
2642
  event: "design.task.read",
@@ -2555,22 +2649,30 @@ var PredictionClient = class {
2555
2649
  } catch (caught) {
2556
2650
  if (input.signal?.aborted === true) throw caught;
2557
2651
  if (deadline.timedOut()) {
2558
- if (attempt === maxAttempts) throw new DesignError("TASK_READ_FAILED", "Task status request timed out within the retry bound", {
2652
+ if (attempt === maxAttempts || input.waitSeconds !== void 0) throw new DesignError("TASK_READ_FAILED", "Task status request timed out within the retry bound", {
2559
2653
  cause: caught,
2560
2654
  status: 408
2561
2655
  });
2562
2656
  lastFailure = caught;
2563
- await this.#waitBeforeRetry(attempt, null, input.signal);
2657
+ await this.#waitBeforeRetry(attempt, null, overall?.signal ?? input.signal);
2564
2658
  continue;
2565
2659
  }
2566
2660
  if (caught instanceof DesignError) throw caught;
2567
2661
  if (isAbortError$1(caught) || attempt === maxAttempts) throw new DesignError("TASK_READ_FAILED", "Task status could not be read within the retry bound", { cause: caught });
2568
2662
  lastFailure = caught;
2569
- await this.#waitBeforeRetry(attempt, null, input.signal);
2663
+ await this.#waitBeforeRetry(attempt, null, overall?.signal ?? input.signal);
2570
2664
  }
2571
2665
  }
2572
2666
  throw new DesignError("TASK_READ_FAILED", "Task status could not be read within the retry bound", { cause: lastFailure });
2573
2667
  }
2668
+ /** Wait for one bounded server observation; does not submit or sleep between snapshots. */
2669
+ async waitTask(input) {
2670
+ return this.readTask({
2671
+ ...input,
2672
+ waitSeconds: input.timeoutSeconds ?? 30,
2673
+ maxAttempts: input.maxAttempts ?? 1
2674
+ });
2675
+ }
2574
2676
  async #waitBeforeRetry(attempt, retryAfter, signal) {
2575
2677
  const delay = Math.min(MAX_INLINE_RETRY_DELAY_MS, Math.max(retryAfter ?? 0, 250 * 2 ** (attempt - 1)));
2576
2678
  this.#log({
@@ -5480,6 +5582,11 @@ var DesignHostController = class {
5480
5582
  #catalogNotice = null;
5481
5583
  #catalogCredentialEpoch = null;
5482
5584
  #pollCursor = 0;
5585
+ #pollInFlight;
5586
+ #pollAbort;
5587
+ #pollEpoch;
5588
+ #observationTail = Promise.resolve();
5589
+ #serializeObservation;
5483
5590
  constructor(options) {
5484
5591
  this.#repository = new DesignTaskRepository({ storage: options.storage });
5485
5592
  this.#resolveCredential = options.resolveCredential;
@@ -5490,6 +5597,11 @@ var DesignHostController = class {
5490
5597
  this.#rememberModel = options.rememberModel;
5491
5598
  this.#fetch = options.fetch ?? globalThis.fetch;
5492
5599
  this.#now = options.now ?? Date.now;
5600
+ this.#serializeObservation = options.serializeObservation ?? ((operation) => {
5601
+ const run = this.#observationTail.then(operation);
5602
+ this.#observationTail = run.catch(() => void 0);
5603
+ return run;
5604
+ });
5493
5605
  }
5494
5606
  async handle(endpoint, payload, signal) {
5495
5607
  signal?.throwIfAborted();
@@ -5509,32 +5621,59 @@ var DesignHostController = class {
5509
5621
  if (!this.#isEnabled()) return false;
5510
5622
  const credential = await this.#resolveCredential();
5511
5623
  if (credential === void 0) return false;
5624
+ if (this.#pollEpoch !== credential.credentialEpoch) this.invalidateWaits();
5625
+ if (this.#pollInFlight !== void 0) return this.#pollInFlight;
5626
+ const abort = new AbortController();
5627
+ this.#pollAbort = abort;
5628
+ this.#pollEpoch = credential.credentialEpoch;
5629
+ const combined = signal === void 0 ? abort.signal : AbortSignal.any([signal, abort.signal]);
5630
+ const run = this.#waitRunning(credential, combined);
5631
+ this.#pollInFlight = run;
5632
+ try {
5633
+ return await run;
5634
+ } finally {
5635
+ if (this.#pollInFlight === run) {
5636
+ this.#pollInFlight = void 0;
5637
+ this.#pollAbort = void 0;
5638
+ }
5639
+ }
5640
+ }
5641
+ invalidateWaits() {
5642
+ this.#pollAbort?.abort();
5643
+ this.#pollAbort = void 0;
5644
+ this.#pollInFlight = void 0;
5645
+ this.#pollEpoch = void 0;
5646
+ }
5647
+ async #waitRunning(credential, signal) {
5512
5648
  const eligible = (await this.#repository.listTasks()).filter((task) => task.taskId !== null && task.credentialEpoch === credential.credentialEpoch && !task.pollBlocked && (task.state === "queued" || task.state === "running" || task.state === "unknown")).filter((task) => task.nextPollAt <= this.#now()).sort((left, right) => left.createdAt - right.createdAt || String(left.taskId).localeCompare(String(right.taskId)));
5513
5649
  const start = eligible.length === 0 ? 0 : this.#pollCursor % eligible.length;
5514
5650
  const batch = circularSlice(eligible, start, 5);
5515
5651
  this.#pollCursor = eligible.length === 0 ? 0 : (start + batch.length) % eligible.length;
5516
- for (const record of batch) {
5517
- if (!this.#isCredentialEpochCurrent(credential.credentialEpoch)) break;
5652
+ await Promise.all(batch.map(async (record) => {
5653
+ if (!this.#isCredentialEpochCurrent(credential.credentialEpoch)) return;
5518
5654
  try {
5519
- const task = await new PredictionClient({ fetch: this.#fetch }).readTask({
5655
+ const task = await new PredictionClient({ fetch: this.#fetch }).waitTask({
5520
5656
  taskId: record.taskId,
5521
5657
  apiKey: credential.value,
5522
5658
  maxAttempts: 1,
5523
5659
  ...signal === void 0 ? {} : { signal }
5524
5660
  });
5525
- await this.#repository.recordTaskObserved(task);
5661
+ await this.#serializeObservation(async () => {
5662
+ if (!signal.aborted && this.#isCredentialEpochCurrent(credential.credentialEpoch)) await this.#repository.recordTaskObserved(task);
5663
+ });
5526
5664
  } catch (error) {
5527
5665
  if (signal?.aborted === true) throw error;
5528
5666
  const failure = classifyPollFailure(error, record.pollAttempt + 1, this.#now());
5529
- await this.#repository.recordPollFailure(record.taskId, failure);
5667
+ await this.#serializeObservation(async () => {
5668
+ if (!signal.aborted && this.#isCredentialEpochCurrent(credential.credentialEpoch)) await this.#repository.recordPollFailure(record.taskId, failure);
5669
+ });
5530
5670
  await this.#markUnauthorized(error, credential.credentialEpoch);
5531
5671
  }
5532
- }
5672
+ }));
5533
5673
  return (await this.#repository.listTasks()).some((task) => task.credentialEpoch === credential.credentialEpoch && !task.pollBlocked && (task.state === "queued" || task.state === "running" || task.state === "unknown"));
5534
5674
  }
5535
5675
  async read(input, signal) {
5536
5676
  const session = this.#session(input.sessionId);
5537
- if (this.#isEnabled()) await this.pollRunning(signal);
5538
5677
  const models = await this.#loadModelsSafely(signal);
5539
5678
  if (session.selectedModelId === null && models.length > 0) {
5540
5679
  const modelId = chooseDefaultModel(models, this.#getLastModel());
@@ -6563,7 +6702,7 @@ function createSchemaTool(controller) {
6563
6702
  function createUploadFileTool(client, resolveCredential) {
6564
6703
  return defineTool({
6565
6704
  name: MODELLIX_MEDIA_UPLOAD_FILE_TOOL,
6566
- description: "Upload one image from the current conversation or session workspace to Modellix and return a temporary URL for media model inputs. Supply exactly one of attachment_id or path. Reuse returned URLs and never retry an unknown upload outcome automatically.",
6705
+ description: "Upload an image attachment from this conversation or a supported image, video, or audio file inside the session workspace. Maximum 16 MiB (16777216 bytes). Supply exactly one of attachment_id or path. Reuse returned URLs and never retry an unknown upload outcome automatically. On quota errors, explicitly select unneeded files for deletion; never clean up automatically.",
6567
6706
  parameters: {
6568
6707
  attachment_id: {
6569
6708
  type: "string",
@@ -6571,7 +6710,7 @@ function createUploadFileTool(client, resolveCredential) {
6571
6710
  },
6572
6711
  path: {
6573
6712
  type: "string",
6574
- description: "Image path inside the current session workspace."
6713
+ description: "Image, video, or audio path inside the current session workspace."
6575
6714
  }
6576
6715
  },
6577
6716
  output: {
@@ -7955,6 +8094,7 @@ var ModellixRuntime = class ModellixRuntime {
7955
8094
  mutate: (operations, expectedRevision) => this.#ctx.settings.mutate(LLM_SETTINGS_NAMESPACE, operations, expectedRevision)
7956
8095
  });
7957
8096
  this.#design = new DesignHostController({
8097
+ serializeObservation: (operation) => this.enqueueDesignWrite(operation),
7958
8098
  storage: designStorage,
7959
8099
  resolveCredential: () => this.resolveUsableCredential(),
7960
8100
  isCredentialEpochCurrent: (epoch) => epoch === this.#credential.credentialEpoch,
@@ -7974,6 +8114,7 @@ var ModellixRuntime = class ModellixRuntime {
7974
8114
  this.#ctx.effect(() => this.#settings.watch((next, previous) => {
7975
8115
  if (this.#closing) return void 0;
7976
8116
  this.#config = next;
8117
+ if (next.credentialEpoch !== previous.credentialEpoch || !next.services.design.enabled) this.#design.invalidateWaits();
7977
8118
  this.#catalog.invalidate();
7978
8119
  if (next.services.design.enabled !== previous.services.design.enabled) {
7979
8120
  this.syncDesignTools(next.services.design.enabled);
@@ -8031,6 +8172,7 @@ var ModellixRuntime = class ModellixRuntime {
8031
8172
  return async () => {
8032
8173
  this.#closing = true;
8033
8174
  this.#lifecycleAbort.abort();
8175
+ this.#design.invalidateWaits();
8034
8176
  this.#disposeDesignTools?.();
8035
8177
  this.#disposeDesignTools = void 0;
8036
8178
  if (this.#designPollTimer !== void 0) clearTimeout(this.#designPollTimer);
@@ -8689,7 +8831,10 @@ var ModellixRuntime = class ModellixRuntime {
8689
8831
  handle: (endpoint, payload, signal) => {
8690
8832
  const operationSignal = signal === void 0 ? this.#lifecycleAbort.signal : AbortSignal.any([signal, this.#lifecycleAbort.signal]);
8691
8833
  if (designEndpointRequestsCredential(endpoint) && !this.credentialIsUsable()) this.requestCredentialRecovery();
8692
- return this.enqueueDesignWrite(() => this.#design.handle(endpoint, payload, operationSignal));
8834
+ return this.enqueueDesignWrite(() => this.#design.handle(endpoint, payload, operationSignal)).then((snapshot) => {
8835
+ this.scheduleDesignPoll(0);
8836
+ return snapshot;
8837
+ });
8693
8838
  }
8694
8839
  });
8695
8840
  return;
@@ -8731,7 +8876,7 @@ var ModellixRuntime = class ModellixRuntime {
8731
8876
  if (this.#closing || this.#designPollTimer !== void 0) return;
8732
8877
  this.#designPollTimer = setTimeout(() => {
8733
8878
  this.#designPollTimer = void 0;
8734
- this.enqueueDesignWrite(() => this.#design.pollRunning(this.#lifecycleAbort.signal)).then((hasRunning) => {
8879
+ this.#design.pollRunning(this.#lifecycleAbort.signal).then((hasRunning) => {
8735
8880
  if (hasRunning) this.scheduleDesignPoll(5e3);
8736
8881
  }).catch(() => {
8737
8882
  if (!this.#closing) this.scheduleDesignPoll(15e3);