dsh-modellix 0.2.0 → 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;
@@ -555,11 +554,14 @@ declare class MediaUploadClient {
555
554
  declare function prepareMediaUploadFromSession(agent: Agent | undefined, attachmentId: string, signal?: AbortSignal): Promise<PreparedMediaUpload>;
556
555
  declare function prepareMediaUploadFromPath(cwd: string | undefined, inputPath: string): Promise<PreparedMediaUpload>;
557
556
  //#endregion
558
- //#region src/design/schema-ir.d.ts
557
+ //#region src/shared/json-value.d.ts
558
+ /** Lossless JSON values shared across Harness versions. */
559
559
  type JsonPrimitive = null | boolean | number | string;
560
- type JsonValue = JsonPrimitive | readonly JsonValue[] | {
561
- readonly [key: string]: JsonValue;
560
+ type JsonValue = JsonPrimitive | JsonValue[] | {
561
+ [key: string]: JsonValue;
562
562
  };
563
+ //#endregion
564
+ //#region src/design/schema-ir.d.ts
563
565
  type UiFieldKind = "string" | "number" | "integer" | "boolean" | "object" | "array" | "media" | "unknown";
564
566
  type UiMediaKind = "image" | "video" | "audio";
565
567
  interface UiConstraints {
@@ -718,6 +720,7 @@ interface ReadPredictionInput {
718
720
  readonly apiKey: string;
719
721
  readonly maxAttempts?: number;
720
722
  readonly signal?: AbortSignal;
723
+ readonly waitSeconds?: number;
721
724
  }
722
725
  declare class PredictionClient {
723
726
  #private;
@@ -726,6 +729,10 @@ declare class PredictionClient {
726
729
  submit(input: SubmitPredictionInput): Promise<PredictionTask>;
727
730
  /** GET retries only transient read failures and is capped at five attempts. */
728
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>;
729
736
  }
730
737
  declare function validateSubmitEndpoint(endpoint: string, modelSlug: string): URL;
731
738
  declare function parsePredictionTask(payload: unknown, expectedTaskId?: string): PredictionTask | null;
@@ -894,6 +901,7 @@ interface CredentialSnapshot {
894
901
  readonly credentialEpoch: number;
895
902
  }
896
903
  interface DesignHostControllerOptions {
904
+ readonly serializeObservation?: (operation: () => Promise<void>) => Promise<void>;
897
905
  readonly storage: StoragePort;
898
906
  readonly resolveCredential: () => Promise<CredentialSnapshot | undefined>;
899
907
  readonly isCredentialEpochCurrent: (credentialEpoch: number) => boolean;
@@ -985,6 +993,7 @@ declare class DesignHostController {
985
993
  constructor(options: DesignHostControllerOptions);
986
994
  handle(endpoint: string, payload: unknown, signal?: AbortSignal): Promise<DesignSnapshotWire>;
987
995
  pollRunning(signal?: AbortSignal): Promise<boolean>;
996
+ invalidateWaits(): void;
988
997
  private read;
989
998
  private refresh;
990
999
  private selectModel;
@@ -1100,6 +1109,7 @@ declare class ModellixRuntime {
1100
1109
  private enqueueDesignWrite;
1101
1110
  private rememberDesignModel;
1102
1111
  private syncDesignTools;
1112
+ private syncWebTools;
1103
1113
  private scheduleDesignPoll;
1104
1114
  }
1105
1115
  //#endregion
@@ -1445,5 +1455,5 @@ interface Config {}
1445
1455
  declare const Config: z<Config>;
1446
1456
  declare function apply(ctx: Context): Promise<void>;
1447
1457
  //#endregion
1448
- export { AUTHENTICATED_CATALOG_URL, ApprovedHttpRequest, AvailableDesignResult, BeginLlmMaterializationInput, BeginOnboardingSaveInput, CURRENT_CONFIG_SCHEMA_VERSION, CacheEntry, CachePort, ClockPort, Config, CredentialBroker, CredentialBrokerOptions, CredentialDescriptor, CredentialEpochConflictError, CredentialMutationCoordinator, CredentialMutationResult, CredentialSource, CredentialState, CredentialValidationError, CredentialVerification, DEFAULT_DESIGN_WAL_KEY, DEFAULT_RESULT_TTL_MS, DEFAULT_WEB_RESPONSE_BYTES, DEFAULT_WEB_SEARCH_MAX_RESULTS, DESIGN_PLANNER_ENDPOINT, DESIGN_PLANNER_MODEL, DesignConfig, DesignError, DesignErrorCode, DesignHostController, DesignHostControllerOptions, DesignLogEvent, DesignLogLevel, DesignMediaCategory, DesignModelSummary, DesignPlannerClient, DesignPlannerClientOptions, DesignPlannerRequest, DesignPlannerResult, DesignPollDiagnosticCode, DesignSchemaIR, DesignSnapshotWire, DesignTaskRecord, DesignTaskRepository, DesignTaskRepositoryOptions, DesignTaskState, DesignToolController, DesignWalEvent, EMPTY_LLM_ROUTE_LEDGER, ExactParameterPatch, FetchPort, HarnessCredentialInfo, HarnessCredentialPort, HeaderRecord, HeaderValue, HttpMethod, HttpPolicyError, HttpRequestPolicyInput, HttpResponseBoundaryError, HttpRetryFailure, InvalidCredentialEpoch, JsonPrimitive, JsonValue, LlmCatalogCache, LlmCatalogCacheOptions, LlmCatalogClient, LlmCatalogClientOptions, LlmCatalogRequestError, LlmConfig, LlmCredentialSnapshot, LlmInterruptedMaterializationEvidence, LlmInterruptedMaterializationResult, LlmMaterializationConflictError, LlmMaterializationReceipt, LlmMaterializationRecovery, LlmMaterializationRollbackError, LlmOwnedEntry, LlmOwnershipConfig, LlmPreparedMaterialization, LlmRegistryBackreadError, LlmRegistryReader, LlmRegistryVerificationOptions, LlmRouteConflictError, LlmRouteLedger, LlmRouteOwnership, LlmRouteOwnershipConfig, LlmRouteOwnershipEntry, LlmSettingsMaterializer, LlmSettingsPort, LoggerPort, MAX_WEB_QUERY_CHARS, MAX_WEB_SEARCH_RESULTS, MAX_WEB_URL_CHARS, MODELLIX_CREDENTIAL_REF, MODELLIX_LLM_BASE_URL, MODELLIX_LLM_MODELS_URL, MODELLIX_LLM_PROVENANCE_FIELD, MODELLIX_LLM_PROVIDER_ID, MODELLIX_MEDIA_GENERATE_TOOL, MODELLIX_MEDIA_GET_RESULT_TOOL, MODELLIX_MEDIA_LIST_TOOL, MODELLIX_MEDIA_PREPARE_TOOL, MODELLIX_MEDIA_SCHEMA_TOOL, MODELLIX_MEDIA_UPLOAD_FILE_TOOL, MODELLIX_ORIGINS, MODELLIX_SETTINGS_NAMESPACE, MODELLIX_WEB_FETCH_ENDPOINT, MODELLIX_WEB_FETCH_TOOL, MODELLIX_WEB_PROVIDER_ID, MODELLIX_WEB_SEARCH_ENDPOINT, MODELLIX_WEB_SEARCH_TOOL, MediaUploadClient, MediaUploadInput, MediaUploadRecord, ModelCatalogClient, ModelCatalogClientOptions, ModelCatalogPage, ModelCatalogQuery, ModelSchemaClient, ModelSchemaClientOptions, ModelSchemaDocument, ModellixDesignDomain, ModellixErrorCode, ModellixErrorContext, ModellixErrorContract, ModellixFailure, ModellixLlmCatalog, ModellixLlmModel, ModellixOriginName, ModellixPiAiRoute, ModellixRuntime, ModellixRuntimeState, ModellixService, ModellixWebContractError, ModellixWebCredentialSnapshot, ModellixWebFetchFailedError, ModellixWebFetchProvider, ModellixWebProviderError, ModellixWebProviderOptions, ModellixWebProviders, ModellixWebRegistry, ModellixWebSearchProvider, NaturalLanguagePlan, OnboardingConfig, OnboardingRecoveryAction, OnboardingRecoveryDecision, OnboardingSaveConflictError, OnboardingSavePhase, OnboardingSaveRecovery, OnboardingStatus, PUBLIC_PORTAL_CATALOG_URL, ParsedFetchFailure, ParsedFetchResponse, ParsedFetchSuccess, ParsedSearchResponse, PersistedLlmRouteOwnership, PiAiModelEntry, PluginConfig, PluginSettingsController, PluginSettingsSchema, PluginSettingsSnapshot, PredictionClient, PredictionClientOptions, PredictionResource, PredictionTask, PredictionTaskStatus, PreparedMediaUpload, REDACTED, ReadPredictionInput, RedactedValue, RequestDeadline, RetentionPolicy, RetryDelayInput, RetryOptions, RetrySuccess, RouteMaterializationPlan, RouteRemovalPlan, SchemaDiagnostic, SchemaParserLimits, ServiceId, ServiceToggles, ServicesConfig, SettingsNamespaceDescriptor, SettingsScopeLike, SettingsServiceLike, SleepPort, StaleLlmCatalogError, StoragePort, SubmitPredictionInput, UiConstraints, UiField, UiFieldKind, UiMediaKind, UiVariant, UnauthorizedTransition, UnsupportedConfigVersionError, VerificationTransition, WebConfig, abandonLlmMaterialization, advanceCredentialEpoch, apply, applyCredentialDescriptor, applyExactPatch, applyNaturalLanguage, applyRuntimeUnauthorized, applyVerificationResult, approveHttpRequest, approveRedirect, beginLlmMaterialization, beginOnboardingSave, buildFetchRequest, buildInvocationBody, buildSearchRequest, completeLlmMaterialization, completeOnboardingSave, computeRetryDelay, createCredentialState, createDefaultConfig, createModellixDesignToolDefinitions, createModellixWebProviders, createModellixWebToolDefinitions, deferOnboarding, deriveModellixSessionId, deriveModellixUserId, executeWithRetry, extractAllowedSubmitUrl, getServiceToggles, inject, isAllowedModellixOrigin, isCredentialInvalidError, isPublicHostname, isRetryableError, isRetryableReadFailure, isValidModellixIdentity, markOnboardingCredentialSaved, materializeDefaults, migrateConfig, missingCredentialDescriptor, modellixDesignDomainSpec, name, normalizeCredentialDescriptor, openDesignStorage, parseCatalogPage, parseDesignSchema, parseFetchResponse, parsePredictionTask, parseResources, parseRetryAfter, parseSearchResponse, planLlmRouteMaterialization, planLlmRouteRemoval, prepareMediaUploadFromPath, prepareMediaUploadFromSession, preserveStoredCredentialAfterCandidateFailure, preserveVerificationAfterTransientFailure, readBoundedResponseJson, readBoundedResponseText, reconcileLlmRouteLedgerAfterInterruption, reconcileOnboardingSave, redactForLog, redactHeaders, redactUrl, registerModellixDesignTools, registerModellixWebProviderInstances, registerModellixWebProviders, registerModellixWebTools, replayDesignWal, requestDeadline, retryAfterFromFailure, selectAvailableResults, setServiceToggles, systemClock, systemSleep, toModellixError, validatePublicHttpUrl, validateSubmitEndpoint, verifyLlmRegistryBackread };
1458
+ export { AUTHENTICATED_CATALOG_URL, ApprovedHttpRequest, AvailableDesignResult, BeginLlmMaterializationInput, BeginOnboardingSaveInput, CURRENT_CONFIG_SCHEMA_VERSION, CacheEntry, CachePort, ClockPort, Config, CredentialBroker, CredentialBrokerOptions, CredentialDescriptor, CredentialEpochConflictError, CredentialMutationCoordinator, CredentialMutationResult, CredentialSource, CredentialState, CredentialValidationError, CredentialVerification, DEFAULT_DESIGN_WAL_KEY, DEFAULT_RESULT_TTL_MS, DEFAULT_WEB_RESPONSE_BYTES, DEFAULT_WEB_SEARCH_MAX_RESULTS, DESIGN_PLANNER_ENDPOINT, DESIGN_PLANNER_MODEL, DesignConfig, DesignError, DesignErrorCode, DesignHostController, DesignHostControllerOptions, DesignLogEvent, DesignLogLevel, DesignMediaCategory, DesignModelSummary, DesignPlannerClient, DesignPlannerClientOptions, DesignPlannerRequest, DesignPlannerResult, DesignPollDiagnosticCode, DesignSchemaIR, DesignSnapshotWire, DesignTaskRecord, DesignTaskRepository, DesignTaskRepositoryOptions, DesignTaskState, DesignToolController, DesignWalEvent, EMPTY_LLM_ROUTE_LEDGER, ExactParameterPatch, FetchPort, HarnessCredentialInfo, HarnessCredentialPort, HeaderRecord, HeaderValue, HttpMethod, HttpPolicyError, HttpRequestPolicyInput, HttpResponseBoundaryError, HttpRetryFailure, InvalidCredentialEpoch, type JsonPrimitive, type JsonValue, LlmCatalogCache, LlmCatalogCacheOptions, LlmCatalogClient, LlmCatalogClientOptions, LlmCatalogRequestError, LlmConfig, LlmCredentialSnapshot, LlmInterruptedMaterializationEvidence, LlmInterruptedMaterializationResult, LlmMaterializationConflictError, LlmMaterializationReceipt, LlmMaterializationRecovery, LlmMaterializationRollbackError, LlmOwnedEntry, LlmOwnershipConfig, LlmPreparedMaterialization, LlmRegistryBackreadError, LlmRegistryReader, LlmRegistryVerificationOptions, LlmRouteConflictError, LlmRouteLedger, LlmRouteOwnership, LlmRouteOwnershipConfig, LlmRouteOwnershipEntry, LlmSettingsMaterializer, LlmSettingsPort, LoggerPort, MAX_WEB_QUERY_CHARS, MAX_WEB_SEARCH_RESULTS, MAX_WEB_URL_CHARS, MODELLIX_CREDENTIAL_REF, MODELLIX_LLM_BASE_URL, MODELLIX_LLM_MODELS_URL, MODELLIX_LLM_PROVENANCE_FIELD, MODELLIX_LLM_PROVIDER_ID, MODELLIX_MEDIA_GENERATE_TOOL, MODELLIX_MEDIA_GET_RESULT_TOOL, MODELLIX_MEDIA_LIST_TOOL, MODELLIX_MEDIA_PREPARE_TOOL, MODELLIX_MEDIA_SCHEMA_TOOL, MODELLIX_MEDIA_UPLOAD_FILE_TOOL, MODELLIX_ORIGINS, MODELLIX_SETTINGS_NAMESPACE, MODELLIX_WEB_FETCH_ENDPOINT, MODELLIX_WEB_FETCH_TOOL, MODELLIX_WEB_PROVIDER_ID, MODELLIX_WEB_SEARCH_ENDPOINT, MODELLIX_WEB_SEARCH_TOOL, MediaUploadClient, MediaUploadInput, MediaUploadRecord, ModelCatalogClient, ModelCatalogClientOptions, ModelCatalogPage, ModelCatalogQuery, ModelSchemaClient, ModelSchemaClientOptions, ModelSchemaDocument, ModellixDesignDomain, ModellixErrorCode, ModellixErrorContext, ModellixErrorContract, ModellixFailure, ModellixLlmCatalog, ModellixLlmModel, ModellixOriginName, ModellixPiAiRoute, ModellixRuntime, ModellixRuntimeState, ModellixService, ModellixWebContractError, ModellixWebCredentialSnapshot, ModellixWebFetchFailedError, ModellixWebFetchProvider, ModellixWebProviderError, ModellixWebProviderOptions, ModellixWebProviders, ModellixWebRegistry, ModellixWebSearchProvider, NaturalLanguagePlan, OnboardingConfig, OnboardingRecoveryAction, OnboardingRecoveryDecision, OnboardingSaveConflictError, OnboardingSavePhase, OnboardingSaveRecovery, OnboardingStatus, PUBLIC_PORTAL_CATALOG_URL, ParsedFetchFailure, ParsedFetchResponse, ParsedFetchSuccess, ParsedSearchResponse, PersistedLlmRouteOwnership, PiAiModelEntry, PluginConfig, PluginSettingsController, PluginSettingsSchema, PluginSettingsSnapshot, PredictionClient, PredictionClientOptions, PredictionResource, PredictionTask, PredictionTaskStatus, PreparedMediaUpload, REDACTED, ReadPredictionInput, RedactedValue, RequestDeadline, RetentionPolicy, RetryDelayInput, RetryOptions, RetrySuccess, RouteMaterializationPlan, RouteRemovalPlan, SchemaDiagnostic, SchemaParserLimits, ServiceId, ServiceToggles, ServicesConfig, SettingsNamespaceDescriptor, SettingsScopeLike, SettingsServiceLike, SleepPort, StaleLlmCatalogError, StoragePort, SubmitPredictionInput, UiConstraints, UiField, UiFieldKind, UiMediaKind, UiVariant, UnauthorizedTransition, UnsupportedConfigVersionError, VerificationTransition, WebConfig, abandonLlmMaterialization, advanceCredentialEpoch, apply, applyCredentialDescriptor, applyExactPatch, applyNaturalLanguage, applyRuntimeUnauthorized, applyVerificationResult, approveHttpRequest, approveRedirect, beginLlmMaterialization, beginOnboardingSave, buildFetchRequest, buildInvocationBody, buildSearchRequest, completeLlmMaterialization, completeOnboardingSave, computeRetryDelay, createCredentialState, createDefaultConfig, createModellixDesignToolDefinitions, createModellixWebProviders, createModellixWebToolDefinitions, deferOnboarding, deriveModellixSessionId, deriveModellixUserId, executeWithRetry, extractAllowedSubmitUrl, getServiceToggles, inject, isAllowedModellixOrigin, isCredentialInvalidError, isPublicHostname, isRetryableError, isRetryableReadFailure, isValidModellixIdentity, markOnboardingCredentialSaved, materializeDefaults, migrateConfig, missingCredentialDescriptor, modellixDesignDomainSpec, name, normalizeCredentialDescriptor, openDesignStorage, parseCatalogPage, parseDesignSchema, parseFetchResponse, parsePredictionTask, parseResources, parseRetryAfter, parseSearchResponse, planLlmRouteMaterialization, planLlmRouteRemoval, prepareMediaUploadFromPath, prepareMediaUploadFromSession, preserveStoredCredentialAfterCandidateFailure, preserveVerificationAfterTransientFailure, readBoundedResponseJson, readBoundedResponseText, reconcileLlmRouteLedgerAfterInterruption, reconcileOnboardingSave, redactForLog, redactHeaders, redactUrl, registerModellixDesignTools, registerModellixWebProviderInstances, registerModellixWebProviders, registerModellixWebTools, replayDesignWal, requestDeadline, retryAfterFromFailure, selectAvailableResults, setServiceToggles, systemClock, systemSleep, toModellixError, validatePublicHttpUrl, validateSubmitEndpoint, verifyLlmRegistryBackread };
1449
1459
  //# sourceMappingURL=index.d.ts.map
package/lib/index.js CHANGED
@@ -2,9 +2,9 @@ import z from "@deepseek-ai/schemastery";
2
2
  import { createHash, randomUUID } from "node:crypto";
3
3
  import { getOrCreateAnonymousUserId } from "@deepseek-ai/dsh-anonymous-user-id";
4
4
  import { credentialRef } from "@deepseek-ai/dsh-credentials";
5
- import { SettingsConflictError, settingsNamespace } from "@deepseek-ai/dsh-settings";
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());
@@ -6215,13 +6354,14 @@ function stableJson(value) {
6215
6354
  //#region src/host/agent-routing-context.ts
6216
6355
  function createModellixRoutingMessage(state) {
6217
6356
  const sections = [];
6357
+ const nonMediaWebRouting = state.webEnabled ? "use the Modellix Web tools for those questions" : "leave those questions to the Harness-native web_search or web_fetch tools when available";
6218
6358
  if (state.webEnabled) sections.push({
6219
6359
  name: "Web routing",
6220
6360
  text: "Use modellix_web_search automatically for current, changing, external, or source-verification questions. Use modellix_web_fetch automatically when the user supplies a public URL or a Modellix search result needs full-page reading. Prefer these explicit Modellix tools over the native web_search and web_fetch tools, and never call both tool families for the same operation. Do not wait for the user to name a tool, do not browse when the user explicitly says not to, and never automatically repeat a failed or unknown Modellix Web request."
6221
6361
  });
6222
6362
  if (state.mediaEnabled) sections.push({
6223
6363
  name: "Media routing",
6224
- text: "Use Modellix media tools automatically only when the user intends to create, edit, transform, animate, upload, or inspect generated image, video, or audio work. Do not use media tools for documentation, pricing, general model-capability, or public API-reference questions; use the Modellix Web tools for those questions unless the user explicitly requests a live Modellix Design model Schema. For media work, use modellix_media_list then modellix_media_schema when a compatible model is not already known. Treat every returned field description and allowed value as authoritative; do not guess, translate, or replace a published parameter format, and omit optional fields when the user did not request them. A request that refers to the previous/current work, conversation attachments, or prior Modellix result URLs, asks to change or animate it, or asks to preserve its composition, subject, identity, or structure is not a new text-to-media request: select an image-edit/image-to-image, image-to-video, or video-to-video model as appropriate, inspect its schema, and place the latest relevant Modellix result URL in the declared media input field. Never replace that source-dependent operation with text-to-image or text-to-video. Reuse usable URLs and upload local inputs with modellix_media_upload_file. After submitting, inspect the task at most once in the same turn. Hard response rule: while a task is nonterminal, ordinary assistant text may say only that the submission was accepted and that the result card and Modellix Design own the current status. It must not say that the task is queued, running, generating, processing, pending, or any equivalent current-state wording, because ordinary assistant text is immutable and becomes stale. Never launch a replacement or parallel generation while a matching task is active unless the user explicitly requests another result, and never repeat an unknown generation or upload outcome."
6364
+ text: `Use Modellix media tools automatically only when the user intends to create, edit, transform, animate, upload, or inspect generated image, video, or audio work. Do not use media tools for documentation, pricing, general model-capability, or public API-reference questions; ${nonMediaWebRouting} unless the user explicitly requests a live Modellix Design model Schema. For media work, use modellix_media_list then modellix_media_schema when a compatible model is not already known. Treat every returned field description and allowed value as authoritative; do not guess, translate, or replace a published parameter format, and omit optional fields when the user did not request them. A request that refers to the previous/current work, conversation attachments, or prior Modellix result URLs, asks to change or animate it, or asks to preserve its composition, subject, identity, or structure is not a new text-to-media request: select an image-edit/image-to-image, image-to-video, or video-to-video model as appropriate, inspect its schema, and place the latest relevant Modellix result URL in the declared media input field. Never replace that source-dependent operation with text-to-image or text-to-video. Reuse usable URLs and upload local inputs with modellix_media_upload_file. After submitting, inspect the task at most once in the same turn. Hard response rule: while a task is nonterminal, ordinary assistant text may say only that the submission was accepted and that the result card and Modellix Design own the current status. It must not say that the task is queued, running, generating, processing, pending, or any equivalent current-state wording, because ordinary assistant text is immutable and becomes stale. Never launch a replacement or parallel generation while a matching task is active unless the user explicitly requests another result, and never repeat an unknown generation or upload outcome.`
6225
6365
  });
6226
6366
  if (sections.length === 0) return null;
6227
6367
  return createUserMessage({
@@ -6562,7 +6702,7 @@ function createSchemaTool(controller) {
6562
6702
  function createUploadFileTool(client, resolveCredential) {
6563
6703
  return defineTool({
6564
6704
  name: MODELLIX_MEDIA_UPLOAD_FILE_TOOL,
6565
- 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.",
6566
6706
  parameters: {
6567
6707
  attachment_id: {
6568
6708
  type: "string",
@@ -6570,7 +6710,7 @@ function createUploadFileTool(client, resolveCredential) {
6570
6710
  },
6571
6711
  path: {
6572
6712
  type: "string",
6573
- description: "Image path inside the current session workspace."
6713
+ description: "Image, video, or audio path inside the current session workspace."
6574
6714
  }
6575
6715
  },
6576
6716
  output: {
@@ -7859,7 +7999,7 @@ var PluginSettingsController = class {
7859
7999
  //#endregion
7860
8000
  //#region src/host/runtime.ts
7861
8001
  const RPC_CHANNEL = "/modellix";
7862
- const LLM_SETTINGS_NAMESPACE = settingsNamespace("llm-pi-ai");
8002
+ const LLM_SETTINGS_NAMESPACE = "llm-pi-ai";
7863
8003
  const LLM_OWNERSHIP_ROLLBACK_FAILED = "MODELLIX_LLM_OWNERSHIP_ROLLBACK_FAILED";
7864
8004
  const LLM_MATERIALIZATION_RECOVERED = "MODELLIX_LLM_MATERIALIZATION_RECOVERED";
7865
8005
  const LLM_MATERIALIZATION_RECOVERY_FAILED = "MODELLIX_LLM_MATERIALIZATION_RECOVERY_FAILED";
@@ -7899,6 +8039,7 @@ var ModellixRuntime = class ModellixRuntime {
7899
8039
  #credentialRecoveryRequestId = null;
7900
8040
  #designPollTimer;
7901
8041
  #disposeDesignTools;
8042
+ #disposeWebTools;
7902
8043
  #closing = false;
7903
8044
  #lifecycleAbort = new AbortController();
7904
8045
  static async create(ctx) {
@@ -7953,6 +8094,7 @@ var ModellixRuntime = class ModellixRuntime {
7953
8094
  mutate: (operations, expectedRevision) => this.#ctx.settings.mutate(LLM_SETTINGS_NAMESPACE, operations, expectedRevision)
7954
8095
  });
7955
8096
  this.#design = new DesignHostController({
8097
+ serializeObservation: (operation) => this.enqueueDesignWrite(operation),
7956
8098
  storage: designStorage,
7957
8099
  resolveCredential: () => this.resolveUsableCredential(),
7958
8100
  isCredentialEpochCurrent: (epoch) => epoch === this.#credential.credentialEpoch,
@@ -7972,11 +8114,13 @@ var ModellixRuntime = class ModellixRuntime {
7972
8114
  this.#ctx.effect(() => this.#settings.watch((next, previous) => {
7973
8115
  if (this.#closing) return void 0;
7974
8116
  this.#config = next;
8117
+ if (next.credentialEpoch !== previous.credentialEpoch || !next.services.design.enabled) this.#design.invalidateWaits();
7975
8118
  this.#catalog.invalidate();
7976
8119
  if (next.services.design.enabled !== previous.services.design.enabled) {
7977
8120
  this.syncDesignTools(next.services.design.enabled);
7978
8121
  if (next.services.design.enabled) this.scheduleDesignPoll(0);
7979
8122
  }
8123
+ if (next.services.web.enabled !== previous.services.web.enabled) this.syncWebTools(next.services.web.enabled);
7980
8124
  if (next.services.llm.enabled !== previous.services.llm.enabled || next.credentialEpoch !== previous.credentialEpoch) return this.enqueueWrite(() => this.reconcileLiveSettings());
7981
8125
  }), "dsh-modellix: live settings snapshot");
7982
8126
  this.#ctx.on("credentials/reference-updated", (updated) => {
@@ -8008,36 +8152,12 @@ var ModellixRuntime = class ModellixRuntime {
8008
8152
  });
8009
8153
  });
8010
8154
  this.#ctx.effect(() => {
8011
- const providers = createModellixWebProviders({
8012
- isEnabled: () => this.#config.services.web.enabled,
8013
- hasCredential: () => this.credentialIsUsable(),
8014
- resolveCredential: async () => {
8015
- const hit = await this.resolveUsableCredential();
8016
- if (hit === void 0) this.requestCredentialRecovery();
8017
- return hit === void 0 ? null : {
8018
- apiKey: hit.value,
8019
- credentialEpoch: hit.credentialEpoch
8020
- };
8021
- },
8022
- getUserId: () => this.#userId,
8023
- isCredentialEpochCurrent: (epoch) => epoch === this.#config.credentialEpoch,
8024
- onCredentialRejected: async (epoch) => {
8025
- if (epoch !== this.#config.credentialEpoch) return;
8026
- this.markCredentialRejected(epoch);
8027
- }
8028
- });
8029
- const disposeProviders = registerModellixWebProviderInstances(this.#ctx.web, providers);
8030
- try {
8031
- const disposeTools = registerModellixWebTools(this.#ctx, providers);
8032
- return () => {
8033
- disposeTools();
8034
- disposeProviders();
8035
- };
8036
- } catch (error) {
8037
- disposeProviders();
8038
- throw error;
8039
- }
8040
- }, "dsh-modellix: Web providers and explicit Modellix tools");
8155
+ this.syncWebTools(this.#config.services.web.enabled);
8156
+ return () => {
8157
+ this.#disposeWebTools?.();
8158
+ this.#disposeWebTools = void 0;
8159
+ };
8160
+ }, "dsh-modellix: explicit Modellix Web tools");
8041
8161
  this.#ctx.on("agent/session-start", ({ agent }) => {
8042
8162
  const message = createModellixRoutingMessage({
8043
8163
  mediaEnabled: this.#config.services.design.enabled && this.credentialIsUsable(),
@@ -8045,12 +8165,14 @@ var ModellixRuntime = class ModellixRuntime {
8045
8165
  });
8046
8166
  if (message !== null) agent.inject(message);
8047
8167
  });
8048
- this.#ctx.effect(() => this.#ctx.connection.rpc.handle(RPC_CHANNEL, (endpoint, payload, signal) => this.handleRpc(endpoint, payload, signal), { authority: "loopback" }), "dsh-modellix: loopback configuration and Design RPC");
8168
+ const connectionRpc = this.#ctx.connection.rpc;
8169
+ this.#ctx.effect(() => connectionRpc.handle(RPC_CHANNEL, (endpoint, payload, signal) => this.handleRpc(endpoint, payload, signal), { authority: "loopback" }), "dsh-modellix: loopback configuration and Design RPC");
8049
8170
  this.#ctx.effect(() => {
8050
8171
  this.scheduleDesignPoll(2e3);
8051
8172
  return async () => {
8052
8173
  this.#closing = true;
8053
8174
  this.#lifecycleAbort.abort();
8175
+ this.#design.invalidateWaits();
8054
8176
  this.#disposeDesignTools?.();
8055
8177
  this.#disposeDesignTools = void 0;
8056
8178
  if (this.#designPollTimer !== void 0) clearTimeout(this.#designPollTimer);
@@ -8709,7 +8831,10 @@ var ModellixRuntime = class ModellixRuntime {
8709
8831
  handle: (endpoint, payload, signal) => {
8710
8832
  const operationSignal = signal === void 0 ? this.#lifecycleAbort.signal : AbortSignal.any([signal, this.#lifecycleAbort.signal]);
8711
8833
  if (designEndpointRequestsCredential(endpoint) && !this.credentialIsUsable()) this.requestCredentialRecovery();
8712
- 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
+ });
8713
8838
  }
8714
8839
  });
8715
8840
  return;
@@ -8719,11 +8844,39 @@ var ModellixRuntime = class ModellixRuntime {
8719
8844
  this.#disposeDesignTools = void 0;
8720
8845
  }
8721
8846
  }
8847
+ syncWebTools(enabled) {
8848
+ if (enabled && this.#disposeWebTools === void 0) {
8849
+ const providers = createModellixWebProviders({
8850
+ isEnabled: () => this.#config.services.web.enabled,
8851
+ hasCredential: () => this.credentialIsUsable(),
8852
+ resolveCredential: async () => {
8853
+ const hit = await this.resolveUsableCredential();
8854
+ if (hit === void 0) this.requestCredentialRecovery();
8855
+ return hit === void 0 ? null : {
8856
+ apiKey: hit.value,
8857
+ credentialEpoch: hit.credentialEpoch
8858
+ };
8859
+ },
8860
+ getUserId: () => this.#userId,
8861
+ isCredentialEpochCurrent: (epoch) => epoch === this.#config.credentialEpoch,
8862
+ onCredentialRejected: async (epoch) => {
8863
+ if (epoch !== this.#config.credentialEpoch) return;
8864
+ this.markCredentialRejected(epoch);
8865
+ }
8866
+ });
8867
+ this.#disposeWebTools = registerModellixWebTools(this.#ctx, providers);
8868
+ return;
8869
+ }
8870
+ if (!enabled && this.#disposeWebTools !== void 0) {
8871
+ this.#disposeWebTools();
8872
+ this.#disposeWebTools = void 0;
8873
+ }
8874
+ }
8722
8875
  scheduleDesignPoll(delayMs) {
8723
8876
  if (this.#closing || this.#designPollTimer !== void 0) return;
8724
8877
  this.#designPollTimer = setTimeout(() => {
8725
8878
  this.#designPollTimer = void 0;
8726
- this.enqueueDesignWrite(() => this.#design.pollRunning(this.#lifecycleAbort.signal)).then((hasRunning) => {
8879
+ this.#design.pollRunning(this.#lifecycleAbort.signal).then((hasRunning) => {
8727
8880
  if (hasRunning) this.scheduleDesignPoll(5e3);
8728
8881
  }).catch(() => {
8729
8882
  if (!this.#closing) this.scheduleDesignPoll(15e3);