@noodleseed/assistant 1.13.0 → 1.15.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/dist/index.cjs CHANGED
@@ -22191,7 +22191,7 @@ function mountAssistantApp(detail, actions, options = {}) {
22191
22191
  const current = options.getDetail?.() ?? detail;
22192
22192
  void bridge.sendSandboxResourceReady({
22193
22193
  html: current.html,
22194
- sandbox: "allow-scripts allow-forms",
22194
+ sandbox: "allow-scripts",
22195
22195
  ...current.resourceMeta ?? {}
22196
22196
  });
22197
22197
  };
@@ -26817,7 +26817,7 @@ var AssistantChatStateStore = class {
26817
26817
  this.#writeText(event.data.delta);
26818
26818
  return;
26819
26819
  case "tool_proposed":
26820
- this.#writeConfirmation(event.data);
26820
+ this.#writeStandaloneData(() => this.#writeConfirmation(event.data));
26821
26821
  return;
26822
26822
  case "interaction_proposed":
26823
26823
  this.#writeData("confirmation", event.data.id, {
@@ -26826,10 +26826,12 @@ var AssistantChatStateStore = class {
26826
26826
  });
26827
26827
  return;
26828
26828
  case "input_requested":
26829
- this.#writeData("input-request", event.data.id, {
26830
- ...event.data,
26831
- status: "pending"
26832
- });
26829
+ this.#writeStandaloneData(
26830
+ () => this.#writeData("input-request", event.data.id, {
26831
+ ...event.data,
26832
+ status: "pending"
26833
+ })
26834
+ );
26833
26835
  return;
26834
26836
  case "tool_completed":
26835
26837
  this.#writeData("tool-result", event.data.id, event.data);
@@ -26936,6 +26938,12 @@ var AssistantChatStateStore = class {
26936
26938
  #writeConfirmation(data) {
26937
26939
  this.#writeData("confirmation", data.id, { ...data, status: "pending" });
26938
26940
  }
26941
+ #writeStandaloneData(write) {
26942
+ const standalone = this.#operation === void 0;
26943
+ if (standalone) this.#startOperation();
26944
+ write();
26945
+ if (standalone) this.#closeOperation("ready");
26946
+ }
26939
26947
  #writeData(name21, id, data) {
26940
26948
  const operation = this.#operation;
26941
26949
  if (!operation || operation.closed) return;
@@ -27268,6 +27276,24 @@ function hasToJsonProperty(value) {
27268
27276
  return false;
27269
27277
  }
27270
27278
 
27279
+ // src/session-source.ts
27280
+ var NOODLE_CLOUD_URL = "https://cloud.noodleseed.dev";
27281
+ var PUBLIC_SESSION_PATH = "/v1/assistant/public-sessions";
27282
+ function resolveSessionSource(options) {
27283
+ const { sessionEndpoint, embedId, serviceUrl } = options;
27284
+ if (Boolean(embedId) === Boolean(sessionEndpoint)) {
27285
+ throw new Error("pass either embedId or sessionEndpoint, not both and not neither");
27286
+ }
27287
+ if (embedId) {
27288
+ const base = (serviceUrl ?? NOODLE_CLOUD_URL).replace(/\/+$/, "");
27289
+ return { kind: "public", url: `${base}${PUBLIC_SESSION_PATH}`, embedId };
27290
+ }
27291
+ return { kind: "exchange", url: sessionEndpoint };
27292
+ }
27293
+ function sessionSourceKey(options) {
27294
+ return `${options.embedId ?? ""}|${options.serviceUrl ?? ""}|${options.sessionEndpoint ?? ""}`;
27295
+ }
27296
+
27271
27297
  // src/transport.ts
27272
27298
  var AssistantTransportError = class extends Error {
27273
27299
  code = "invalid_response";
@@ -27395,6 +27421,19 @@ function isAbortError2(error51) {
27395
27421
  }
27396
27422
 
27397
27423
  // src/client.ts
27424
+ async function refusalCode(response) {
27425
+ try {
27426
+ const body = await response.clone().json();
27427
+ const code = body.code;
27428
+ return typeof code === "string" ? code : void 0;
27429
+ } catch {
27430
+ return void 0;
27431
+ }
27432
+ }
27433
+ var UNRETRYABLE_SERVICE_CODES = /* @__PURE__ */ new Set([
27434
+ "daily_turn_budget_exhausted",
27435
+ "daily_session_budget_exhausted"
27436
+ ]);
27398
27437
  var AssistantClientError = class extends Error {
27399
27438
  detail;
27400
27439
  constructor(detail, message, options) {
@@ -27404,7 +27443,7 @@ var AssistantClientError = class extends Error {
27404
27443
  }
27405
27444
  };
27406
27445
  var DefaultAssistantClient = class {
27407
- #sessionEndpoint;
27446
+ #source;
27408
27447
  #fetch;
27409
27448
  #listeners = /* @__PURE__ */ new Set();
27410
27449
  #chat = new AssistantChatStateStore();
@@ -27414,9 +27453,9 @@ var DefaultAssistantClient = class {
27414
27453
  #context;
27415
27454
  #modelContext;
27416
27455
  #active;
27456
+ #pendingAppInteractions = /* @__PURE__ */ new Map();
27417
27457
  constructor(options) {
27418
- if (!options.sessionEndpoint) throw new Error("sessionEndpoint is required");
27419
- this.#sessionEndpoint = options.sessionEndpoint;
27458
+ this.#source = resolveSessionSource(options);
27420
27459
  this.#fetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
27421
27460
  this.#context = options.context ? { ...options.context } : void 0;
27422
27461
  this.#modelContext = options.modelContext ? copyAssistantModelContext(options.modelContext) : void 0;
@@ -27455,6 +27494,7 @@ var DefaultAssistantClient = class {
27455
27494
  }
27456
27495
  await this.#singleFlight(async (signal) => {
27457
27496
  await this.#runChatOperation(signal, async () => {
27497
+ const pendingApp = this.#pendingAppInteractions.get(id);
27458
27498
  const session = this.#session;
27459
27499
  if (!session) {
27460
27500
  throw clientError("confirmation_expired", "assistant session is not active", false);
@@ -27474,32 +27514,67 @@ var DefaultAssistantClient = class {
27474
27514
  ...resolution.action === "accept" && resolution.content !== void 0 ? { content: resolution.content } : {}
27475
27515
  } : { id };
27476
27516
  this.#emit({ event: "interaction_started", data: { id, action: resolution.action } });
27477
- const response = await this.#request(
27478
- endpoint,
27479
- {
27480
- method: "POST",
27481
- headers: authorizedHeaders(session.token, "text/event-stream"),
27482
- body: JSON.stringify(body),
27483
- signal
27484
- },
27485
- "confirmation_failed"
27486
- );
27487
- if (response.status === 401) {
27488
- this.#session = void 0;
27489
- this.#emit({ event: "session_expired", data: {} });
27490
- }
27491
- if (!response.ok) {
27492
- const serverCode = await readStableErrorCode(response);
27493
- const expired = response.status === 401 || response.status === 409;
27494
- throw clientError(
27495
- serverCode ?? (expired ? "confirmation_expired" : "confirmation_failed"),
27496
- `Assistant interaction failed (${response.status})`,
27497
- false,
27498
- response.status
27517
+ try {
27518
+ const response = await this.#request(
27519
+ endpoint,
27520
+ {
27521
+ method: "POST",
27522
+ headers: authorizedHeaders(session.token, "text/event-stream"),
27523
+ body: JSON.stringify(body),
27524
+ signal
27525
+ },
27526
+ "confirmation_failed"
27499
27527
  );
27528
+ if (response.status === 401) {
27529
+ this.#session = void 0;
27530
+ this.#emit({ event: "session_expired", data: {} });
27531
+ }
27532
+ if (!response.ok) {
27533
+ const serverCode = await readStableErrorCode(response);
27534
+ const expired = response.status === 401 || response.status === 409;
27535
+ throw clientError(
27536
+ serverCode ?? (expired ? "confirmation_expired" : "confirmation_failed"),
27537
+ `Assistant interaction failed (${response.status})`,
27538
+ false,
27539
+ response.status
27540
+ );
27541
+ }
27542
+ let completedResult;
27543
+ let nextInteractionId;
27544
+ await this.#consume(response, (event) => {
27545
+ if (event.event === "tool_completed" && event.data.id === id) {
27546
+ completedResult = event.data.result;
27547
+ }
27548
+ if ((event.event === "tool_proposed" || event.event === "input_requested") && event.data.id !== id) {
27549
+ nextInteractionId = event.data.id;
27550
+ }
27551
+ });
27552
+ this.#emit({ event: "interaction_completed", data: { id, action: resolution.action } });
27553
+ if (pendingApp) {
27554
+ this.#pendingAppInteractions.delete(id);
27555
+ if (nextInteractionId) {
27556
+ this.#pendingAppInteractions.set(nextInteractionId, pendingApp);
27557
+ } else if (resolution.action !== "accept") {
27558
+ pendingApp.resolve(appInteractionStopped(resolution.action));
27559
+ } else if (completedResult !== void 0) {
27560
+ pendingApp.resolve(appToolResult(completedResult));
27561
+ } else {
27562
+ pendingApp.reject(
27563
+ clientError(
27564
+ "invalid_response",
27565
+ "Assistant app interaction completed without a tool result",
27566
+ false
27567
+ )
27568
+ );
27569
+ }
27570
+ }
27571
+ } catch (error51) {
27572
+ if (pendingApp && this.#pendingAppInteractions.get(id) === pendingApp) {
27573
+ this.#pendingAppInteractions.delete(id);
27574
+ pendingApp.reject(error51);
27575
+ }
27576
+ throw error51;
27500
27577
  }
27501
- await this.#consume(response);
27502
- this.#emit({ event: "interaction_completed", data: { id, action: resolution.action } });
27503
27578
  });
27504
27579
  });
27505
27580
  }
@@ -27540,12 +27615,29 @@ var DefaultAssistantClient = class {
27540
27615
  response.status
27541
27616
  );
27542
27617
  }
27543
- return response.json();
27618
+ const value = await response.json();
27619
+ const interaction = parseAppInteraction(value);
27620
+ if (!interaction) return value;
27621
+ return new Promise((resolve2, reject) => {
27622
+ const id = interaction.data.id;
27623
+ const replaced = this.#pendingAppInteractions.get(id);
27624
+ if (replaced) {
27625
+ replaced.reject(
27626
+ clientError("invalid_response", "Assistant returned a duplicate app interaction", false)
27627
+ );
27628
+ }
27629
+ this.#pendingAppInteractions.set(id, { resolve: resolve2, reject });
27630
+ this.#emit(interaction);
27631
+ void this.#chat.flush();
27632
+ });
27544
27633
  }
27545
27634
  appSandboxUrl() {
27546
27635
  return this.#session?.endpoints.sandbox;
27547
27636
  }
27548
27637
  resetSession() {
27638
+ this.#rejectPendingAppInteractions(
27639
+ clientError("confirmation_expired", "assistant session is not active", false)
27640
+ );
27549
27641
  this.#session = void 0;
27550
27642
  this.#emit({ event: "session_reset", data: {} });
27551
27643
  }
@@ -27579,11 +27671,14 @@ var DefaultAssistantClient = class {
27579
27671
  return;
27580
27672
  }
27581
27673
  if (!response.ok) {
27674
+ const serviceCode = await refusalCode(response);
27582
27675
  throw clientError(
27583
27676
  "turn_failed",
27584
27677
  `Assistant turn failed (${response.status})`,
27585
27678
  false,
27586
- response.status
27679
+ response.status,
27680
+ void 0,
27681
+ serviceCode
27587
27682
  );
27588
27683
  }
27589
27684
  await this.#consume(response);
@@ -27622,23 +27717,25 @@ var DefaultAssistantClient = class {
27622
27717
  }
27623
27718
  }
27624
27719
  async #createSession(signal) {
27625
- const response = await this.#request(
27626
- this.#sessionEndpoint,
27627
- {
27628
- method: "POST",
27629
- headers: { Accept: "application/json", "Content-Type": "application/json" },
27630
- body: JSON.stringify(this.#context ? { context: this.#context } : {}),
27631
- credentials: "same-origin",
27632
- signal
27633
- },
27634
- "session_failed"
27635
- );
27720
+ const source = this.#source;
27721
+ const response = await this.#requestSession(source, {
27722
+ method: "POST",
27723
+ headers: { Accept: "application/json", "Content-Type": "application/json" },
27724
+ body: JSON.stringify(
27725
+ source.kind === "public" ? { embedId: source.embedId } : this.#context ? { context: this.#context } : {}
27726
+ ),
27727
+ credentials: source.kind === "public" ? "omit" : "same-origin",
27728
+ signal
27729
+ });
27636
27730
  if (!response.ok) {
27731
+ const serviceCode = await refusalCode(response);
27637
27732
  throw clientError(
27638
27733
  "session_failed",
27639
27734
  `Assistant session failed (${response.status})`,
27640
- true,
27641
- response.status
27735
+ serviceCode === void 0 || !UNRETRYABLE_SERVICE_CODES.has(serviceCode),
27736
+ response.status,
27737
+ void 0,
27738
+ serviceCode
27642
27739
  );
27643
27740
  }
27644
27741
  let value;
@@ -27666,12 +27763,13 @@ var DefaultAssistantClient = class {
27666
27763
  });
27667
27764
  return session;
27668
27765
  }
27669
- async #consume(response) {
27766
+ async #consume(response, observe) {
27670
27767
  let streamError;
27671
27768
  try {
27672
27769
  await consumeAssistantEvents(response, (event) => {
27673
27770
  const clientEvent = toAssistantClientEvent(event);
27674
27771
  this.#emit(clientEvent);
27772
+ observe?.(clientEvent);
27675
27773
  if (clientEvent.event !== "error" || typeof clientEvent.data.code !== "string") return;
27676
27774
  streamError = {
27677
27775
  code: clientEvent.data.code,
@@ -27694,6 +27792,34 @@ var DefaultAssistantClient = class {
27694
27792
  );
27695
27793
  }
27696
27794
  }
27795
+ /**
27796
+ * The mint, with the one failure a public page hits that an in-app embed cannot.
27797
+ *
27798
+ * A page that loads the embed script and then blocks `connect-src` fails here as a bare network
27799
+ * rejection — no status, no body, nothing to read. "Assistant request failed" sends a developer
27800
+ * hunting through their own code; naming the directive and the origin ends the search. The message
27801
+ * covers a plain outage too, because from inside the browser the two are indistinguishable.
27802
+ */
27803
+ async #requestSession(source, init) {
27804
+ try {
27805
+ return await this.#fetch(source.url, init);
27806
+ } catch (error51) {
27807
+ if (init.signal?.aborted) throw error51;
27808
+ if (source.kind !== "public") {
27809
+ throw clientError("session_failed", "Assistant request failed", true, void 0, {
27810
+ cause: error51
27811
+ });
27812
+ }
27813
+ throw clientError(
27814
+ "session_failed",
27815
+ `Assistant could not reach ${new URL(source.url).origin}. If the page sets a Content-Security-Policy, allow that origin in connect-src (and in script-src and frame-src).`,
27816
+ true,
27817
+ void 0,
27818
+ { cause: error51 },
27819
+ "blocked_by_page"
27820
+ );
27821
+ }
27822
+ }
27697
27823
  async #request(input, init, failureCode) {
27698
27824
  try {
27699
27825
  return await this.#fetch(input, init);
@@ -27740,6 +27866,10 @@ var DefaultAssistantClient = class {
27740
27866
  }
27741
27867
  }
27742
27868
  }
27869
+ #rejectPendingAppInteractions(error51) {
27870
+ for (const pending of this.#pendingAppInteractions.values()) pending.reject(error51);
27871
+ this.#pendingAppInteractions.clear();
27872
+ }
27743
27873
  };
27744
27874
  function createAssistantClient(options) {
27745
27875
  return new DefaultAssistantClient(options);
@@ -27751,6 +27881,30 @@ function authorizedHeaders(token, accept) {
27751
27881
  Accept: accept
27752
27882
  };
27753
27883
  }
27884
+ function parseAppInteraction(value) {
27885
+ if (!isRecord4(value) || !isRecord4(value.interaction)) return void 0;
27886
+ const event = value.interaction.event;
27887
+ const data = value.interaction.data;
27888
+ if (event !== "tool_proposed" && event !== "input_requested" || !isRecord4(data)) {
27889
+ return void 0;
27890
+ }
27891
+ const parsed = toAssistantClientEvent({ event, data });
27892
+ return parsed.event === "tool_proposed" || parsed.event === "input_requested" ? parsed : void 0;
27893
+ }
27894
+ function appToolResult(result) {
27895
+ const text3 = typeof result === "string" ? result : JSON.stringify(result);
27896
+ return result !== null && typeof result === "object" && !Array.isArray(result) ? {
27897
+ content: [{ type: "text", text: text3 }],
27898
+ structuredContent: result,
27899
+ isError: false
27900
+ } : { content: [{ type: "text", text: text3 }], isError: false };
27901
+ }
27902
+ function appInteractionStopped(action) {
27903
+ return {
27904
+ content: [{ type: "text", text: `interaction_${action}` }],
27905
+ isError: true
27906
+ };
27907
+ }
27754
27908
  function parseSession(value) {
27755
27909
  if (!isRecord4(value) || !isRecord4(value.endpoints)) {
27756
27910
  throw clientError("session_failed", "Assistant session response is invalid", true);
@@ -27787,9 +27941,14 @@ async function readStableErrorCode(response) {
27787
27941
  if (!isRecord4(value) || typeof value.code !== "string") return void 0;
27788
27942
  return /^[A-Za-z0-9_.-]{1,64}$/.test(value.code) ? value.code : void 0;
27789
27943
  }
27790
- function clientError(code, message, retryable, status, options) {
27944
+ function clientError(code, message, retryable, status, options, serviceCode) {
27791
27945
  return new AssistantClientError(
27792
- { code, ...status === void 0 ? {} : { status }, retryable },
27946
+ {
27947
+ code,
27948
+ ...status === void 0 ? {} : { status },
27949
+ retryable,
27950
+ ...serviceCode === void 0 ? {} : { serviceCode }
27951
+ },
27793
27952
  message,
27794
27953
  options
27795
27954
  );
@@ -30966,7 +31125,6 @@ var AssistantElementEventController = class {
30966
31125
  return;
30967
31126
  }
30968
31127
  if (name21 === "tool_proposed" || name21 === "interaction_proposed") {
30969
- if (!this.#streamKind) return;
30970
31128
  this.#appendToolProposal(
30971
31129
  String(data.id ?? ""),
30972
31130
  String(data.tool ?? "action"),
@@ -30978,7 +31136,6 @@ var AssistantElementEventController = class {
30978
31136
  return;
30979
31137
  }
30980
31138
  if (name21 === "input_requested" && isRecord5(data.requestedSchema)) {
30981
- if (!this.#streamKind) return;
30982
31139
  this.#appendInputRequest(
30983
31140
  String(data.id ?? ""),
30984
31141
  String(data.message ?? "More information is required"),
@@ -31882,6 +32039,10 @@ var NoodleAssistantElement = class extends HTMLElementBase {
31882
32039
  #appearanceStyleSnapshot = /* @__PURE__ */ new Map();
31883
32040
  #scroll = new AssistantElementScrollController();
31884
32041
  sessionEndpoint = "";
32042
+ /** Public mount: the non-secret embed id `noodle deploy` printed. Mutually exclusive with the above. */
32043
+ embedId = "";
32044
+ /** Only for a dev or self-hosted service; the published snippet needs no origin. */
32045
+ serviceUrl = "";
31885
32046
  get appearance() {
31886
32047
  return this.#hostAppearance;
31887
32048
  }
@@ -31936,6 +32097,8 @@ var NoodleAssistantElement = class extends HTMLElementBase {
31936
32097
  connectedCallback() {
31937
32098
  if (!this.shadowRoot) this.attachShadow({ mode: "open" });
31938
32099
  if (!this.sessionEndpoint) this.sessionEndpoint = this.getAttribute("session-endpoint") ?? "";
32100
+ if (!this.embedId) this.embedId = this.getAttribute("embed-id") ?? "";
32101
+ if (!this.serviceUrl) this.serviceUrl = this.getAttribute("service-url") ?? "";
31939
32102
  this.#media = globalThis.matchMedia?.("(prefers-color-scheme: dark)");
31940
32103
  this.#media?.addEventListener("change", this.#handleSystemTheme);
31941
32104
  document.addEventListener("keydown", this.#handleDocumentKeydown);
@@ -32107,26 +32270,31 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32107
32270
  }
32108
32271
  };
32109
32272
  #ensureClient() {
32110
- if (this.#client && this.#clientEndpoint === this.sessionEndpoint) return this.#client;
32273
+ const key = sessionSourceKey(this);
32274
+ if (this.#client && this.#clientEndpoint === key) return this.#client;
32111
32275
  this.#client?.abort();
32112
32276
  this.#unsubscribeClient?.();
32113
32277
  this.#sessionBootstrap = void 0;
32114
- const client = createAssistantClient({
32115
- sessionEndpoint: this.sessionEndpoint,
32278
+ const behavior = {
32116
32279
  ...this.#context ? { context: this.#context } : {},
32117
32280
  clientContext: browserClientContext,
32118
32281
  ...this.#pageContext ? { pageContext: this.#pageContext } : {},
32119
32282
  ...this.#modelContext ? { modelContext: this.#modelContext } : {}
32120
- });
32283
+ };
32284
+ const client = this.embedId ? createAssistantClient({
32285
+ embedId: this.embedId,
32286
+ ...this.serviceUrl ? { serviceUrl: this.serviceUrl } : {},
32287
+ ...behavior
32288
+ }) : createAssistantClient({ sessionEndpoint: this.sessionEndpoint, ...behavior });
32121
32289
  this.#client = client;
32122
- this.#clientEndpoint = this.sessionEndpoint;
32290
+ this.#clientEndpoint = key;
32123
32291
  this.#unsubscribeClient = client.subscribe((event) => {
32124
32292
  if (this.#client === client) this.#handleClientEvent(event);
32125
32293
  });
32126
32294
  return client;
32127
32295
  }
32128
32296
  #primeSession() {
32129
- if (!this.sessionEndpoint) return;
32297
+ if (!this.sessionEndpoint && !this.embedId) return;
32130
32298
  const client = this.#ensureClient();
32131
32299
  const generation = this.#conversationGeneration;
32132
32300
  this.#setSessionState("loading");
@@ -32250,23 +32418,30 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32250
32418
  }
32251
32419
  const detail = error51 instanceof AssistantClientError ? error51.detail : { code: fallbackCode, retryable: false };
32252
32420
  this.#appendRecoveryError(
32253
- detail.code === "session_expired" ? this.#appearance.labels.sessionExpired : this.#appearance.labels.unavailable
32421
+ detail.code === "session_expired" ? this.#appearance.labels.sessionExpired : this.#appearance.labels.unavailable,
32422
+ // A spent or switched-off daily budget offers no retry. Reconnecting cannot succeed, and every
32423
+ // open tab trying is exactly the load the cap was set to refuse — so the button goes, not just
32424
+ // the alarming words. A spent *session* keeps it: reconnect mints a fresh one, which is the fix.
32425
+ detail.retryable !== false || detail.serviceCode === void 0
32254
32426
  );
32255
32427
  this.#dispatchError(detail);
32256
32428
  }
32257
- #appendRecoveryError(message) {
32429
+ #appendRecoveryError(message, offerRetry = true) {
32258
32430
  if (!this.#messages) return;
32259
32431
  this.#messages.querySelector(".conversation-error")?.remove();
32260
32432
  const error51 = document.createElement("section");
32261
32433
  error51.className = "conversation-error";
32262
- error51.setAttribute("role", "alert");
32434
+ error51.setAttribute("role", offerRetry ? "alert" : "status");
32263
32435
  const text3 = document.createElement("p");
32264
32436
  text3.textContent = message;
32265
- const retry = document.createElement("button");
32266
- retry.type = "button";
32267
- retry.textContent = this.#appearance.labels.reconnect;
32268
- retry.addEventListener("click", () => this.reconnect());
32269
- error51.append(text3, retry);
32437
+ error51.append(text3);
32438
+ if (offerRetry) {
32439
+ const retry = document.createElement("button");
32440
+ retry.type = "button";
32441
+ retry.textContent = this.#appearance.labels.reconnect;
32442
+ retry.addEventListener("click", () => this.reconnect());
32443
+ error51.append(retry);
32444
+ }
32270
32445
  this.#messages.append(error51);
32271
32446
  this.#revealLatest();
32272
32447
  }