@mclawnet/agent 0.6.29 → 0.6.31

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.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=hub-connection-reconnect.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hub-connection-reconnect.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/hub-connection-reconnect.test.ts"],"names":[],"mappings":""}
@@ -46,6 +46,13 @@ export interface SpawnOptions {
46
46
  mcpConfigPath?: string;
47
47
  /** Memory system role ID. When set, SessionManager auto-injects memory prompt + MCP config */
48
48
  roleId?: string;
49
+ /**
50
+ * Optional first user input. When set, SessionManager uses this as the
51
+ * semantic-retrieval query for `buildMemorySection` (Pipeline A) so the
52
+ * injected memory section is relevant to what the user is about to ask.
53
+ * Falls back to an empty query (importance-top-K) when omitted.
54
+ */
55
+ initialUserInput?: string;
49
56
  /** Extra directories to mount via --add-dir */
50
57
  additionalDirs?: string[];
51
58
  /** Optional allowlist of tool names to forward to the backend (PR3.5). */
@@ -1 +1 @@
1
- {"version":3,"file":"backend-adapter.d.ts","sourceRoot":"","sources":["../src/backend-adapter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"backend-adapter.d.ts","sourceRoot":"","sources":["../src/backend-adapter.ts"],"names":[],"mappings":"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"}
@@ -470,6 +470,7 @@ async function handleSwarmControl(coord, msg, opts) {
470
470
 
471
471
  // src/hub-connection.ts
472
472
  import { createLogger as createLogger2, previewFields } from "@mclawnet/logger";
473
+ import { DEFAULT_ASSISTANT_ROLE_ID } from "@mclawnet/memory";
473
474
  var log2 = createLogger2({ module: "agent" });
474
475
  var HubConnection = class {
475
476
  ws = null;
@@ -559,13 +560,13 @@ var HubConnection = class {
559
560
  }
560
561
  connect() {
561
562
  if (this.destroyed) return;
562
- if (this.ws || this.reconnectTimer) return;
563
+ if (this.reconnectTimer) return;
564
+ if (this.ws && (this.ws.readyState === WebSocket.CONNECTING || this.ws.readyState === WebSocket.OPEN)) {
565
+ return;
566
+ }
563
567
  this.cleanupConnection();
564
568
  this.authState = "pending";
565
- this.ws = new WebSocket(this.hubUrl);
566
- this.ws.on("open", () => {
567
- this.reconnectDelay = DEFAULT_RECONNECT_MS;
568
- });
569
+ this.ws = new WebSocket(this.hubUrl, { handshakeTimeout: 1e4 });
569
570
  this.ws.on("message", (raw) => {
570
571
  let data;
571
572
  try {
@@ -589,6 +590,7 @@ var HubConnection = class {
589
590
  log2.info({ agentId: data.agentId }, "registered with hub");
590
591
  this.authState = "authenticated";
591
592
  this.agentId = data.agentId ?? null;
593
+ this.reconnectDelay = DEFAULT_RECONNECT_MS;
592
594
  this.startHeartbeat();
593
595
  this.onConnectCb?.(this.agentId);
594
596
  this.tryRecoverSwarms();
@@ -597,10 +599,16 @@ var HubConnection = class {
597
599
  }
598
600
  if (this.authState === "authenticated") {
599
601
  this.lastAckAt = Date.now();
602
+ if (data && data.type === "heartbeat_ack") {
603
+ return;
604
+ }
600
605
  if (this.handleSessionMessage(data)) return;
601
606
  this.onMessage?.(data);
602
607
  }
603
608
  });
609
+ this.ws.on("pong", () => {
610
+ this.lastAckAt = Date.now();
611
+ });
604
612
  this.ws.on("close", (code, reason) => {
605
613
  log2.warn({ code, reason: reason.toString() }, "disconnected from hub");
606
614
  this.stopHeartbeat();
@@ -616,6 +624,18 @@ var HubConnection = class {
616
624
  this.ws.on("error", (err) => {
617
625
  log2.error({ err }, "ws connection error");
618
626
  this.onError?.(err);
627
+ if (this.ws) {
628
+ const dying = this.ws;
629
+ this.ws = null;
630
+ try {
631
+ dying.removeAllListeners();
632
+ dying.terminate();
633
+ } catch {
634
+ }
635
+ this.stopHeartbeat();
636
+ this.authState = "pending";
637
+ this.scheduleReconnect();
638
+ }
619
639
  });
620
640
  }
621
641
  send(data) {
@@ -1001,10 +1021,12 @@ var HubConnection = class {
1001
1021
  workDir,
1002
1022
  resumeId,
1003
1023
  useBrainCore,
1004
- roleId: "role-__assistant__",
1024
+ roleId: DEFAULT_ASSISTANT_ROLE_ID,
1005
1025
  maxOutputTokens
1006
1026
  }).then(() => {
1007
- sm.sendInput(sessionId, content);
1027
+ sm.sendUserInput(sessionId, content).catch((err) => {
1028
+ log2.warn({ err, sessionId }, "sendUserInput failed");
1029
+ });
1008
1030
  }).catch((err) => {
1009
1031
  this.send({
1010
1032
  type: "session.error",
@@ -1016,7 +1038,9 @@ var HubConnection = class {
1016
1038
  if (sm.isHealthy(sessionId)) {
1017
1039
  log2.info({ sessionId }, "claude.execute: reusing healthy session");
1018
1040
  log2.debug({ sessionId, ...previewFields(content) }, "claude.execute: input");
1019
- sm.sendInput(sessionId, content);
1041
+ sm.sendUserInput(sessionId, content).catch((err) => {
1042
+ log2.warn({ err, sessionId }, "sendUserInput failed");
1043
+ });
1020
1044
  } else if (sm.hasSession(sessionId) && claudeSessionId) {
1021
1045
  const recommendedMax = sm.getRecommendedMaxOutputTokens(sessionId);
1022
1046
  log2.warn({ sessionId, claudeSessionId }, "claude.execute: session unhealthy, recreating with --resume");
@@ -1056,12 +1080,12 @@ var HubConnection = class {
1056
1080
  return true;
1057
1081
  }
1058
1082
  if (msg.type === "session.create") {
1059
- log2.info({ sessionId: msg.sessionId, roleId: "role-__assistant__" }, "session.create with memory injection");
1083
+ log2.info({ sessionId: msg.sessionId, roleId: DEFAULT_ASSISTANT_ROLE_ID }, "session.create with memory injection");
1060
1084
  this.sessionManager.createSession({
1061
1085
  sessionId: msg.sessionId,
1062
1086
  workDir: msg.workDir,
1063
1087
  resumeId: msg.resumeId,
1064
- roleId: "role-__assistant__"
1088
+ roleId: DEFAULT_ASSISTANT_ROLE_ID
1065
1089
  }).then((claudeSessionId) => {
1066
1090
  this.send({
1067
1091
  type: "session.created",
@@ -1088,7 +1112,9 @@ var HubConnection = class {
1088
1112
  { sessionId: msg.sessionId, ...previewFields(msg.content) },
1089
1113
  "claude.input"
1090
1114
  );
1091
- this.sessionManager.sendInput(msg.sessionId, msg.content);
1115
+ this.sessionManager.sendUserInput(msg.sessionId, msg.content).catch((err) => {
1116
+ log2.warn({ err, sessionId: msg.sessionId }, "sendUserInput failed");
1117
+ });
1092
1118
  return true;
1093
1119
  }
1094
1120
  return false;
@@ -1148,6 +1174,10 @@ var HubConnection = class {
1148
1174
  return;
1149
1175
  }
1150
1176
  this.send({ type: "heartbeat", ts: Date.now() });
1177
+ try {
1178
+ this.ws?.ping();
1179
+ } catch {
1180
+ }
1151
1181
  }, this.heartbeatInterval);
1152
1182
  }
1153
1183
  stopHeartbeat() {
@@ -1236,12 +1266,15 @@ var HubConnection = class {
1236
1266
  scheduleReconnect() {
1237
1267
  if (this.destroyed) return;
1238
1268
  if (this.reconnectTimer) return;
1239
- log2.warn({ delayMs: this.reconnectDelay }, "reconnecting to hub...");
1269
+ const cap = Math.min(this.reconnectDelay, this.maxReconnectDelay);
1270
+ const base = Math.min(DEFAULT_RECONNECT_MS, cap);
1271
+ const delay = Math.floor(base + Math.random() * Math.max(0, cap - base));
1272
+ log2.warn({ delayMs: delay, capMs: cap }, "reconnecting to hub...");
1240
1273
  this.reconnectTimer = setTimeout(() => {
1241
1274
  this.reconnectTimer = null;
1242
1275
  this.connect();
1243
- }, this.reconnectDelay);
1244
- this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
1276
+ }, delay);
1277
+ this.reconnectDelay = Math.min(cap * 2, this.maxReconnectDelay);
1245
1278
  }
1246
1279
  cleanup() {
1247
1280
  this.cleanupConnection();
@@ -1678,7 +1711,12 @@ function makeRealSwarmStarter(_deps) {
1678
1711
 
1679
1712
  // src/session-manager.ts
1680
1713
  import { createLogger as createLogger5, previewFields as previewFields2 } from "@mclawnet/logger";
1681
- import { buildMemorySection } from "@mclawnet/memory";
1714
+ import {
1715
+ buildMemorySection,
1716
+ EmbeddingService,
1717
+ createEmbeddingProviders,
1718
+ initDatabase
1719
+ } from "@mclawnet/memory";
1682
1720
  import { MAX_TOKENS_LADDER, clampLadderIndex } from "@mclawnet/shared";
1683
1721
 
1684
1722
  // src/skill-loader.ts
@@ -2040,6 +2078,13 @@ function isPidAlive(pid) {
2040
2078
  }
2041
2079
 
2042
2080
  // src/session-manager.ts
2081
+ var sharedEmbeddingService = null;
2082
+ function getSharedEmbeddingService() {
2083
+ if (sharedEmbeddingService) return sharedEmbeddingService;
2084
+ const db = initDatabase();
2085
+ sharedEmbeddingService = new EmbeddingService(db, createEmbeddingProviders());
2086
+ return sharedEmbeddingService;
2087
+ }
2043
2088
  var log5 = createLogger5({ module: "agent/session-manager" });
2044
2089
  var DEFAULT_MAX_PROCESSES = 30;
2045
2090
  var MAX_PROCESSES = Number(process.env.CLAWNET_MAX_PROCESSES) || DEFAULT_MAX_PROCESSES;
@@ -2071,6 +2116,13 @@ var SessionManager = class {
2071
2116
  // correctly routed to the "spawn new + --resume" branch instead of trying
2072
2117
  // to write to a process we are about to kill.
2073
2118
  aborting = /* @__PURE__ */ new Set();
2119
+ // Sessions whose exit was triggered by an explicit close/abort/closeAll —
2120
+ // used by the onExit hook to label the resulting exit event as `expected`
2121
+ // (so SwarmCoordinator can skip flipping the role to `crashed`). The set is
2122
+ // populated *before* `adapter.stop()` runs and drained inside the onExit
2123
+ // handler. A leftover entry would only matter if a session id were reused,
2124
+ // which createSession already forbids (line 193 throws on duplicate).
2125
+ expectedExits = /* @__PURE__ */ new Set();
2074
2126
  idleSweepTimer = null;
2075
2127
  // PR-A: effective sweeper config. Initialized from env at construct time
2076
2128
  // and overridable per-instance via startIdleSweeper(overrides) — the
@@ -2106,6 +2158,14 @@ var SessionManager = class {
2106
2158
  onSessionError;
2107
2159
  onSessionStarted;
2108
2160
  onBeforeClose;
2161
+ /**
2162
+ * Fires whenever the underlying child process exits — both expected
2163
+ * (closeSession/abortSession) and unexpected (crash, OOM, external SIGKILL).
2164
+ * `expected` lets the listener distinguish so a swarm coordinator can flip
2165
+ * the role to `crashed` only on unplanned exits. Wired in start.ts to route
2166
+ * swarm-role exits to SwarmCoordinator.handleRoleCrashed.
2167
+ */
2168
+ onSessionExit;
2109
2169
  // PR-A: classifies a sessionId as 'chat' or 'swarm-role'. Injected by
2110
2170
  // start.ts via SwarmCoordinator.isSwarmSession to keep SessionManager
2111
2171
  // independent of the swarm package. Defaults to 'chat' if absent — safe
@@ -2119,6 +2179,7 @@ var SessionManager = class {
2119
2179
  this.onSessionError = options.onSessionError;
2120
2180
  this.onSessionStarted = options.onSessionStarted;
2121
2181
  this.onBeforeClose = options.onBeforeClose;
2182
+ this.onSessionExit = options.onSessionExit;
2122
2183
  this.classify = options.classify ?? (() => "chat");
2123
2184
  this.checkpointPath = options.checkpointPath ?? null;
2124
2185
  this.checkpointDebounceMs = options.checkpointDebounceMs ?? 5e3;
@@ -2130,19 +2191,26 @@ var SessionManager = class {
2130
2191
  if (this.sessions.size >= MAX_PROCESSES) {
2131
2192
  throw new SessionLimitReachedError(this.snapshotPoolForError());
2132
2193
  }
2133
- if (options.roleId) {
2194
+ if (options.workDir) {
2134
2195
  try {
2135
- const memorySection = buildMemorySection(options.roleId);
2136
- const roleHint = `
2137
-
2138
- [Memory Context] Your roleId is "${options.roleId}". Always use this roleId when calling memory_search, memory_store, memory_stats, or memory_reflect.`;
2139
- options.systemPrompt = options.systemPrompt ? `${memorySection}${roleHint}
2196
+ const query = options.initialUserInput ?? "";
2197
+ const memorySection = await buildMemorySection({
2198
+ query,
2199
+ workDir: options.workDir,
2200
+ embeddingService: getSharedEmbeddingService()
2201
+ });
2202
+ options.systemPrompt = options.systemPrompt ? `${memorySection}
2140
2203
 
2141
- ${options.systemPrompt}` : `${memorySection}${roleHint}`;
2142
- log5.debug({ roleId: options.roleId, sessionId: options.sessionId }, "memory prompt + roleId hint injected");
2204
+ ${options.systemPrompt}` : memorySection;
2205
+ log5.debug({ workDir: options.workDir, sessionId: options.sessionId }, "memory prompt injected");
2143
2206
  } catch (err) {
2144
- log5.warn({ err, roleId: options.roleId }, "failed to build memory section, proceeding without");
2207
+ log5.warn({ err, workDir: options.workDir }, "failed to build memory section, proceeding without");
2145
2208
  }
2209
+ } else {
2210
+ log5.warn(
2211
+ { sessionId: options.sessionId, roleId: options.roleId },
2212
+ "session created without workDir \u2014 memory injection skipped (all memories would be cross-workdir; pass workDir to enable scoped retrieval)"
2213
+ );
2146
2214
  }
2147
2215
  try {
2148
2216
  const notice = getPendingNotification();
@@ -2160,6 +2228,12 @@ ${notice.text}`;
2160
2228
  } catch (err) {
2161
2229
  log5.debug({ err }, "failed to inject pending notification");
2162
2230
  }
2231
+ if (this.classify(options.sessionId) === "chat") {
2232
+ const ideaHint = `
2233
+
2234
+ [Idea Capture] When you propose a concrete user-actionable idea, feature suggestion, or follow-up TODO, additionally emit a single tag like \`<idea title="short title" body="one-paragraph context"/>\` at the end of that paragraph. Keep it concise and use it sparingly \u2014 only for things the user would plausibly want to save and revisit. The tag itself will be hidden from the chat view.`;
2235
+ options.systemPrompt = options.systemPrompt ? `${options.systemPrompt}${ideaHint}` : ideaHint.trimStart();
2236
+ }
2163
2237
  try {
2164
2238
  const process2 = await this.adapter.spawn(options);
2165
2239
  this.sessions.set(options.sessionId, process2);
@@ -2257,13 +2331,37 @@ ${notice.text}`;
2257
2331
  }
2258
2332
  this.adapter.onExit?.(process2, (code) => {
2259
2333
  if (this.sessions.get(options.sessionId) === process2) {
2334
+ const expected = this.expectedExits.delete(options.sessionId);
2260
2335
  this.sessions.delete(options.sessionId);
2261
2336
  this.sessionMeta.delete(options.sessionId);
2262
2337
  this.activelyExecuting.delete(options.sessionId);
2263
2338
  this.conversationBuffer.delete(options.sessionId);
2264
2339
  this.scheduleCheckpoint();
2265
- log5.warn({ sessionId: options.sessionId, exitCode: code }, "backend process exited unexpectedly, evicted from session map");
2266
- this.onSessionError(options.sessionId, `backend process exited (code=${code ?? "null"})`);
2340
+ if (!expected) {
2341
+ log5.warn({ sessionId: options.sessionId, exitCode: code }, "backend process exited unexpectedly, evicted from session map");
2342
+ this.onSessionError(options.sessionId, `backend process exited (code=${code ?? "null"})`);
2343
+ } else {
2344
+ log5.debug({ sessionId: options.sessionId, exitCode: code }, "backend process exited as expected");
2345
+ }
2346
+ try {
2347
+ this.onSessionExit?.(options.sessionId, {
2348
+ code: code ?? null,
2349
+ expected,
2350
+ ...expected ? {} : { reason: `exit code=${code ?? "null"}` }
2351
+ });
2352
+ } catch (err) {
2353
+ log5.warn({ err, sessionId: options.sessionId }, "onSessionExit listener threw");
2354
+ }
2355
+ } else {
2356
+ const expected = this.expectedExits.delete(options.sessionId);
2357
+ try {
2358
+ this.onSessionExit?.(options.sessionId, {
2359
+ code: code ?? null,
2360
+ expected
2361
+ });
2362
+ } catch (err) {
2363
+ log5.warn({ err, sessionId: options.sessionId }, "onSessionExit listener threw");
2364
+ }
2267
2365
  }
2268
2366
  });
2269
2367
  return process2.id;
@@ -2288,10 +2386,44 @@ ${notice.text}`;
2288
2386
  );
2289
2387
  this.adapter.send(process2, input);
2290
2388
  }
2389
+ /**
2390
+ * User-input variant of sendInput: best-effort prepend a `<memory-context>`
2391
+ * block sourced from semantic retrieval (Pipeline B). Falls back to plain
2392
+ * sendInput if retrieval errors or exceeds the 200ms budget — we never
2393
+ * block the user's turn on memory.
2394
+ */
2395
+ async sendUserInput(sessionId, content) {
2396
+ const meta = this.sessionMeta.get(sessionId);
2397
+ const workDir = meta?.workDir;
2398
+ let prefixed = content;
2399
+ try {
2400
+ const section = await Promise.race([
2401
+ buildMemorySection({
2402
+ query: content,
2403
+ workDir,
2404
+ embeddingService: getSharedEmbeddingService()
2405
+ }),
2406
+ new Promise(
2407
+ (resolve) => setTimeout(() => resolve(""), 200)
2408
+ )
2409
+ ]);
2410
+ if (section && section.trim().length > 0) {
2411
+ prefixed = `<memory-context>
2412
+ ${section}
2413
+ </memory-context>
2414
+
2415
+ ${content}`;
2416
+ }
2417
+ } catch (err) {
2418
+ log5.debug({ err, sessionId }, "sendUserInput: memory retrieval skipped");
2419
+ }
2420
+ this.sendInput(sessionId, prefixed);
2421
+ }
2291
2422
  async abortSession(sessionId) {
2292
2423
  const process2 = this.sessions.get(sessionId);
2293
2424
  if (!process2) return;
2294
2425
  this.aborting.add(sessionId);
2426
+ this.expectedExits.add(sessionId);
2295
2427
  this.conversationBuffer.delete(sessionId);
2296
2428
  this.sessions.delete(sessionId);
2297
2429
  this.sessionMeta.delete(sessionId);
@@ -2312,6 +2444,7 @@ ${notice.text}`;
2312
2444
  });
2313
2445
  }
2314
2446
  this.conversationBuffer.delete(sessionId);
2447
+ this.expectedExits.add(sessionId);
2315
2448
  this.sessions.delete(sessionId);
2316
2449
  this.sessionMeta.delete(sessionId);
2317
2450
  this.activelyExecuting.delete(sessionId);
@@ -2328,6 +2461,7 @@ ${notice.text}`;
2328
2461
  });
2329
2462
  }
2330
2463
  this.conversationBuffer.delete(sessionId);
2464
+ this.expectedExits.add(sessionId);
2331
2465
  this.sessions.delete(sessionId);
2332
2466
  this.sessionMeta.delete(sessionId);
2333
2467
  this.activelyExecuting.delete(sessionId);
@@ -2992,11 +3126,12 @@ function createSwarmAwareSessionStartedHandler(deps) {
2992
3126
  // src/start.ts
2993
3127
  import { createLogger as createLogger9 } from "@mclawnet/logger";
2994
3128
  import {
2995
- initDatabase,
3129
+ initDatabase as initDatabase2,
2996
3130
  MemoryStore,
2997
- EmbeddingService,
2998
- createEmbeddingProviders,
2999
- distillConversation
3131
+ EmbeddingService as EmbeddingService2,
3132
+ createEmbeddingProviders as createEmbeddingProviders2,
3133
+ distillConversation,
3134
+ DEFAULT_ASSISTANT_ROLE_ID as DEFAULT_ASSISTANT_ROLE_ID2
3000
3135
  } from "@mclawnet/memory";
3001
3136
  import {
3002
3137
  SkillStore,
@@ -3061,9 +3196,9 @@ async function startAgent(options) {
3061
3196
  let skillStore = null;
3062
3197
  let memoryDb = null;
3063
3198
  try {
3064
- memoryDb = initDatabase(dbPath);
3199
+ memoryDb = initDatabase2(dbPath);
3065
3200
  memoryStore = new MemoryStore(memoryDb);
3066
- embeddingService = new EmbeddingService(memoryDb, createEmbeddingProviders());
3201
+ embeddingService = new EmbeddingService2(memoryDb, createEmbeddingProviders2());
3067
3202
  skillStore = new SkillStore(clawnetDir);
3068
3203
  evolutionPipeline = new EvolutionPipeline(clawnetDir);
3069
3204
  } catch (err) {
@@ -3075,7 +3210,7 @@ async function startAgent(options) {
3075
3210
  try {
3076
3211
  await distillConversation(
3077
3212
  messages,
3078
- "role-__assistant__",
3213
+ DEFAULT_ASSISTANT_ROLE_ID2,
3079
3214
  memoryStore,
3080
3215
  embeddingService
3081
3216
  );
@@ -3086,7 +3221,7 @@ async function startAgent(options) {
3086
3221
  if (!skillStore || !evolutionPipeline) return;
3087
3222
  const skills = skillStore.scan();
3088
3223
  if (skills.length === 0) return;
3089
- const recent = memoryStore.getMemoriesByRole("role-__assistant__", "working");
3224
+ const recent = memoryStore.getRecentMemoriesByRole(DEFAULT_ASSISTANT_ROLE_ID2, { limit: 50 });
3090
3225
  const refs = recent.map((m) => ({
3091
3226
  id: m.id,
3092
3227
  type: m.type,
@@ -3140,6 +3275,15 @@ async function startAgent(options) {
3140
3275
  error
3141
3276
  });
3142
3277
  },
3278
+ onSessionExit: (sessionId, info) => {
3279
+ if (info.expected) return;
3280
+ if (!swarmCoordinator?.isSwarmSession(sessionId)) return;
3281
+ try {
3282
+ swarmCoordinator.handleRoleCrashed(sessionId, info.reason ?? `exit code=${info.code ?? "null"}`);
3283
+ } catch (err) {
3284
+ log9.warn({ err, sessionId }, "swarmCoordinator.handleRoleCrashed threw");
3285
+ }
3286
+ },
3143
3287
  onBeforeClose,
3144
3288
  // PR-A: classify session kind for the idle sweeper. SessionManager stays
3145
3289
  // independent of the swarm package; we hand it a closure that defers to
@@ -3197,4 +3341,4 @@ export {
3197
3341
  FsBridge,
3198
3342
  startAgent
3199
3343
  };
3200
- //# sourceMappingURL=chunk-Y4J44CKF.js.map
3344
+ //# sourceMappingURL=chunk-U5CD3OZ3.js.map