ocuclaw 1.3.0 → 1.3.2

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.
Files changed (44) hide show
  1. package/README.md +3 -1
  2. package/dist/config/runtime-config-session-title-model.test.js +22 -0
  3. package/dist/config/runtime-config.js +24 -15
  4. package/dist/domain/debug-store.js +18 -0
  5. package/dist/domain/glasses-display-system-prompt.js +52 -0
  6. package/dist/domain/glasses-display-system-prompt.test.js +44 -0
  7. package/dist/domain/glasses-ui-system-prompt.js +6 -22
  8. package/dist/domain/glasses-ui-system-prompt.test.js +13 -0
  9. package/dist/domain/prompt-channel-fragments.js +32 -0
  10. package/dist/domain/prompt-channel-fragments.test.js +70 -0
  11. package/dist/gateway/gateway-timing-ledger.js +15 -3
  12. package/dist/gateway/openclaw-client.js +80 -3
  13. package/dist/index.js +22 -0
  14. package/dist/runtime/channel-two-hook.js +36 -0
  15. package/dist/runtime/container-env.js +41 -0
  16. package/dist/runtime/display-toggle-states.js +98 -0
  17. package/dist/runtime/plugin-version-service.js +23 -0
  18. package/dist/runtime/register-session-title-distiller.js +100 -0
  19. package/dist/runtime/relay-core.js +307 -68
  20. package/dist/runtime/relay-service.js +120 -13
  21. package/dist/runtime/relay-worker-entry.js +26 -0
  22. package/dist/runtime/relay-worker-protocol.js +0 -4
  23. package/dist/runtime/relay-worker-supervisor.js +43 -79
  24. package/dist/runtime/relay-worker-transport.js +41 -0
  25. package/dist/runtime/session-service.js +159 -15
  26. package/dist/runtime/session-title-distiller-budget.js +36 -0
  27. package/dist/runtime/session-title-distiller-helpers.js +130 -0
  28. package/dist/runtime/session-title-distiller.js +354 -0
  29. package/dist/runtime/session-title-record.js +21 -0
  30. package/dist/runtime/stable-prompt-snapshot.js +119 -0
  31. package/dist/tools/glasses-ui-cron.js +9 -3
  32. package/dist/tools/glasses-ui-paint-floor.js +10 -3
  33. package/dist/tools/glasses-ui-recipes.js +13 -178
  34. package/dist/tools/glasses-ui-surfaces.js +8 -1
  35. package/dist/tools/glasses-ui-tool-description.test.js +16 -0
  36. package/dist/tools/glasses-ui-tool.js +98 -60
  37. package/dist/tools/session-title-tool.js +14 -76
  38. package/dist/tools/session-title-tool.test.js +53 -0
  39. package/dist/version.js +2 -2
  40. package/openclaw.plugin.json +9 -0
  41. package/package.json +6 -4
  42. package/skills/glasses-ui/SKILL.md +163 -0
  43. package/dist/runtime/downstream-server.js +0 -2057
  44. package/dist/runtime/plugin-update-service.js +0 -216
@@ -1,12 +1,33 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { stripAllTaggedSpans } from "../domain/tagged-span-strip.js";
4
+ import { createDisplayToggleTracker } from "./display-toggle-states.js";
5
+ import { decideTitleWrite, isUserOrigin } from "./session-title-record.js";
6
+ import { createDistillerBudget } from "./session-title-distiller-budget.js";
4
7
 
5
8
  const SESSION_FIRST_USER_CACHE_FILE = "session-first-user-cache.json";
6
9
  const SESSION_TITLE_CACHE_FILE = "session-title-cache.json";
7
10
  const SESSION_PIN_CACHE_FILE = "ocuclaw-session-pins.json";
8
11
  const PIN_CAP_PER_KIND = 20;
9
12
 
13
+ /**
14
+ * Greeting-eliciting content appended to /new and /reset so OpenClaw runs an
15
+ * agent turn (the new-session "welcome"). OpenClaw 2026.6.x ("make bare reset
16
+ * commands fast", gateway commit 2c6a3f6b04) made a BARE /new or /reset return
17
+ * a synchronous ack and run NO agent turn — so a bare reset no longer produces
18
+ * a welcome and leaves the glasses stuck on the "starting new session"
19
+ * placeholder. Sending "/new <prompt>" / "/reset <prompt>" routes through the
20
+ * gateway's normal agent path (content = the prompt) and elicits the welcome.
21
+ * The wording matches the prompt OpenClaw used to inject itself, so the
22
+ * existing synthetic-session-starter filters (conversation-state display hide +
23
+ * isSyntheticSessionStarter title/preview filter) keep it hidden from the
24
+ * transcript and session titles. NOTE: newSession() does NOT call
25
+ * conversationState.addMessage("user", ...), so this content never renders as a
26
+ * user bubble on the glasses.
27
+ */
28
+ export const NEW_SESSION_GREETING_PROMPT =
29
+ "A new session was started via /new or /reset. Execute your Session Startup sequence now - read the required files before responding to the user. If BOOTSTRAP.md exists in the provided Project Context, read it and follow its instructions first. Then greet the user in your configured persona, if one is provided. Be yourself - use your defined voice, mannerisms, and mood. Keep it to 1-3 sentences and ask what they want to do. If the runtime model differs from default_model in the system prompt, mention the default model. Do not mention internal steps, files, tools, or reasoning.";
30
+
10
31
 
11
32
  function normalizeLogger(logger) {
12
33
  if (!logger || typeof logger !== "object") {
@@ -150,6 +171,14 @@ export function createSessionService(opts = {}) {
150
171
  const sessionTitleByKey = loadSessionTitleCache();
151
172
  /** @type {Map<string, boolean>} Per-session Neural Session Names toggle state. */
152
173
  const neuralSessionNamesEnabledByKey = new Map();
174
+ /** Per-session display-feature (emoji/pace) toggle states: frozen start + latest.
175
+ * The frozen start-state persists to stateDir so a relay restart can't lose it
176
+ * (the Channel-1 snapshot persists too — see stable-prompt-snapshot). */
177
+ const displayToggleTracker = createDisplayToggleTracker({ stateDir: opts.stateDir });
178
+ /** Per-session SKIP-exempt budget for the background title distiller. Owned
179
+ * here (alongside the title record + toggle tracker) so a logical session
180
+ * reset clears all per-session distiller state in one place. */
181
+ const distillerBudget = createDistillerBudget({});
153
182
 
154
183
  /** Path for session pin metadata cache file. */
155
184
  const sessionPinCachePath = resolveSessionPinCachePath(opts.stateDir);
@@ -762,10 +791,17 @@ export function createSessionService(opts = {}) {
762
791
  function resolveRowTitle(sessionKey, row) {
763
792
  const cached = getSessionTitle(sessionKey);
764
793
  if (cached !== null) return cached;
765
- // row.displayName: upstream session-row label per
766
- // openclaw/docs/reference/session-management-compaction.md §169.
767
- if (row && typeof row.displayName === "string") {
768
- const trimmed = row.displayName.trim();
794
+ // Upstream session-row label: `label` on 2026.6.x rows; older hosts
795
+ // (≤5.27-era docs, session-management-compaction.md §169) used
796
+ // `displayName` accept both.
797
+ const rawLabel =
798
+ row && typeof row.label === "string"
799
+ ? row.label
800
+ : row && typeof row.displayName === "string"
801
+ ? row.displayName
802
+ : "";
803
+ {
804
+ const trimmed = rawLabel.trim();
769
805
  if (trimmed) return trimmed;
770
806
  }
771
807
  return null;
@@ -959,6 +995,7 @@ export function createSessionService(opts = {}) {
959
995
  sessionPinByKey.delete(key);
960
996
  sessionTitleByKey.delete(key);
961
997
  firstSentUserMessageBySession.delete(key);
998
+ distillerBudget.clear(key);
962
999
  deleted.push(key);
963
1000
  } catch (err) {
964
1001
  failed.push({ key, reason: err?.message ?? "unknown" });
@@ -1266,6 +1303,11 @@ export function createSessionService(opts = {}) {
1266
1303
  return entry ? entry.title : null;
1267
1304
  }
1268
1305
 
1306
+ function getSessionTitleRecord(sessionKey) {
1307
+ const entry = sessionTitleByKey.get(sessionKey);
1308
+ return entry ? { ...entry } : null;
1309
+ }
1310
+
1269
1311
  function setSessionTitle(sessionKey, title, opts) {
1270
1312
  if (typeof sessionKey !== "string" || !sessionKey.trim()) {
1271
1313
  return { ok: false, code: "invalid_session_key" };
@@ -1274,19 +1316,26 @@ export function createSessionService(opts = {}) {
1274
1316
  return { ok: false, code: "invalid_title" };
1275
1317
  }
1276
1318
  const trimmed = title.trim();
1277
- const setByUser = !!(opts && opts.userSet === true);
1319
+ // Back-compat: callers passing { userSet:true } map to the user_tool origin.
1320
+ const origin =
1321
+ opts && typeof opts.origin === "string" && opts.origin
1322
+ ? opts.origin
1323
+ : opts && opts.userSet === true
1324
+ ? "user_tool"
1325
+ : "topic_distiller";
1278
1326
  const previous = sessionTitleByKey.get(sessionKey);
1279
-
1280
- if (!setByUser && previous && previous.userSet === true) {
1281
- return { ok: false, code: "session_user_locked" };
1327
+ const decision = decideTitleWrite(previous, origin);
1328
+ if (!decision.allowed) {
1329
+ return { ok: false, code: decision.code };
1282
1330
  }
1283
-
1284
1331
  const replaced = !!previous;
1285
- const nextUserSet = setByUser || (previous && previous.userSet === true);
1332
+ const nextUserSet = decision.nextUserSet;
1333
+ const setByUser = isUserOrigin(origin);
1286
1334
  sessionTitleByKey.set(sessionKey, {
1287
1335
  title: trimmed,
1288
1336
  setAtMs: Date.now(),
1289
1337
  userSet: !!nextUserSet,
1338
+ origin,
1290
1339
  });
1291
1340
  pruneSessionTitleEntries(sessionTitleByKey);
1292
1341
  persistSessionTitleCache();
@@ -1296,15 +1345,26 @@ export function createSessionService(opts = {}) {
1296
1345
  setByUser ? "session_title_set_by_user" : "session_title_set",
1297
1346
  "info",
1298
1347
  { sessionKey },
1299
- () => ({ sessionKey, title: trimmed, replaced, userSet: !!nextUserSet }),
1348
+ () => ({ sessionKey, title: trimmed, replaced, userSet: !!nextUserSet, origin }),
1300
1349
  );
1301
1350
  // Fire-and-forget upstream mirror.
1351
+ if (!isUpstreamConnected()) {
1352
+ emitDebug(
1353
+ "relay.session",
1354
+ "session_title_upstream_mirror_skipped",
1355
+ "debug",
1356
+ { sessionKey },
1357
+ () => ({ reason: "upstream_disconnected", origin }),
1358
+ );
1359
+ }
1302
1360
  if (isUpstreamConnected()) {
1303
1361
  resolveSessionCanonicalKey(sessionKey)
1304
1362
  .then((canonicalKey) =>
1363
+ // 2026.6.x strict schema: the session title field is `label`
1364
+ // (5.27-era `displayName` is rejected as an unexpected property).
1305
1365
  gatewayBridge.request("sessions.patch", {
1306
1366
  key: canonicalKey,
1307
- displayName: trimmed,
1367
+ label: trimmed,
1308
1368
  }),
1309
1369
  )
1310
1370
  .catch((err) => {
@@ -1346,6 +1406,78 @@ export function createSessionService(opts = {}) {
1346
1406
  return cached === undefined ? true : cached;
1347
1407
  }
1348
1408
 
1409
+ function recordDisplayToggleStates(sessionKey, states) {
1410
+ if (typeof sessionKey !== "string" || !sessionKey.trim()) return;
1411
+ displayToggleTracker.record(sessionKey, states);
1412
+ }
1413
+ function getDisplayStartStates(sessionKey) {
1414
+ return displayToggleTracker.getStart(sessionKey);
1415
+ }
1416
+ function getDisplayCurrentStates(sessionKey) {
1417
+ return displayToggleTracker.getCurrent(sessionKey);
1418
+ }
1419
+ function clearDisplayToggleStates(sessionKey) {
1420
+ displayToggleTracker.clear(sessionKey);
1421
+ }
1422
+
1423
+ function getDistillerBudget() {
1424
+ return distillerBudget;
1425
+ }
1426
+ function clearDistillerBudget(sessionKey) {
1427
+ if (typeof sessionKey === "string" && sessionKey.trim()) {
1428
+ distillerBudget.clear(sessionKey);
1429
+ }
1430
+ }
1431
+ // Drop the stored title record for a session AND clear the upstream display
1432
+ // name. setSessionTitle mirrors the title to the upstream session displayName,
1433
+ // and session-list rendering falls back to that displayName when the local
1434
+ // record is gone — so a local-only delete would let the old title reappear on
1435
+ // the next sessions refresh. deleteSessions removes it for genuine deletes;
1436
+ // this is for a reused-key logical reset (/new, /reset).
1437
+ function clearSessionTitle(sessionKey) {
1438
+ if (typeof sessionKey !== "string" || !sessionKey.trim()) return;
1439
+ const hadTitle = sessionTitleByKey.delete(sessionKey);
1440
+ if (!hadTitle) return;
1441
+ persistSessionTitleCache();
1442
+ invalidateSessionsCache();
1443
+ if (isUpstreamConnected()) {
1444
+ resolveSessionCanonicalKey(sessionKey)
1445
+ .then((canonicalKey) =>
1446
+ gatewayBridge.request("sessions.patch", { key: canonicalKey, label: null }),
1447
+ )
1448
+ .catch((err) => {
1449
+ emitDebug(
1450
+ "relay.session",
1451
+ "session_title_upstream_clear_failed",
1452
+ "debug",
1453
+ { sessionKey },
1454
+ () => ({ message: err && err.message ? err.message : String(err) }),
1455
+ );
1456
+ });
1457
+ }
1458
+ }
1459
+
1460
+ // Clear ALL per-session state keyed to a conversation that must not bleed into
1461
+ // a fresh conversation reusing the same session key (/new, /reset). Centralized
1462
+ // so reset paths can't miss a piece (title + upstream name, toggle states,
1463
+ // distiller budget, the first-user-message marker the distiller gate reads, and
1464
+ // the per-session feature toggle).
1465
+ function clearLogicalSessionState(sessionKey) {
1466
+ if (typeof sessionKey !== "string" || !sessionKey.trim()) return;
1467
+ clearSessionTitle(sessionKey);
1468
+ displayToggleTracker.clear(sessionKey);
1469
+ distillerBudget.clear(sessionKey);
1470
+ // Clear BOTH the first-user marker AND the derived preview cache, and persist
1471
+ // the deletion — recordFirstSentUserMessage writes the marker to disk, so a
1472
+ // local-only delete would let a relay/plugin restart reload a stale
1473
+ // "user already spoke" marker for the reused key (the distiller gate reads
1474
+ // this, and the session-list preview reads the derived cache).
1475
+ const hadMarker = firstSentUserMessageBySession.delete(sessionKey);
1476
+ firstUserMessageCache.delete(sessionKey);
1477
+ if (hadMarker) persistFirstSentUserMessageCache();
1478
+ neuralSessionNamesEnabledByKey.delete(sessionKey);
1479
+ }
1480
+
1349
1481
  function isSyntheticSessionStarter(text) {
1350
1482
  if (!text) return false;
1351
1483
  if (
@@ -1575,9 +1707,11 @@ export function createSessionService(opts = {}) {
1575
1707
  onStatusChanged();
1576
1708
  }
1577
1709
  if (sendResetCommand && isUpstreamConnected()) {
1578
- gatewayBridge.sendMessage("/new", sessionKey).catch((err) => {
1579
- logger.error(`[relay] Failed to send /new for new session: ${err.message}`);
1580
- });
1710
+ gatewayBridge
1711
+ .sendMessage(`/new ${NEW_SESSION_GREETING_PROMPT}`, sessionKey)
1712
+ .catch((err) => {
1713
+ logger.error(`[relay] Failed to send /new for new session: ${err.message}`);
1714
+ });
1581
1715
  }
1582
1716
  return { sessionKey, pages };
1583
1717
  }
@@ -1625,11 +1759,21 @@ export function createSessionService(opts = {}) {
1625
1759
  clearPendingInitialConfig,
1626
1760
  getSessions,
1627
1761
  getSessionTitle,
1762
+ getSessionTitleRecord,
1628
1763
  getSessionsByExactKeys,
1629
1764
  hasRecordedFirstUserMessage,
1630
1765
  isNeuralSessionNamesEnabled,
1766
+ isEvenAiSessionKey,
1631
1767
  isSessionUserLocked,
1632
1768
  recordNeuralSessionNamesEnabled,
1769
+ recordDisplayToggleStates,
1770
+ getDisplayStartStates,
1771
+ getDisplayCurrentStates,
1772
+ clearDisplayToggleStates,
1773
+ getDistillerBudget,
1774
+ clearDistillerBudget,
1775
+ clearSessionTitle,
1776
+ clearLogicalSessionState,
1633
1777
  setSessionTitle,
1634
1778
  switchToSession,
1635
1779
  newSession,
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Per-session distiller budget. SKIPs are free; only consecutive errors and a
3
+ * total untitled-turn ceiling bound attempts; an applied title ends attempts.
4
+ */
5
+ export function createDistillerBudget(opts = {}) {
6
+ const maxErr = Number.isFinite(opts.maxConsecutiveErrors) ? opts.maxConsecutiveErrors : 3;
7
+ const ceiling = Number.isFinite(opts.untitledTurnCeiling) ? opts.untitledTurnCeiling : 25;
8
+ /** @type {Map<string,{consecErr:number, turns:number, done:boolean}>} */
9
+ const byKey = new Map();
10
+
11
+ function get(k) {
12
+ let s = byKey.get(k);
13
+ if (!s) { s = { consecErr: 0, turns: 0, done: false }; byKey.set(k, s); }
14
+ return s;
15
+ }
16
+
17
+ return {
18
+ recordTurn(sessionKey) { get(sessionKey).turns += 1; },
19
+ canRun(sessionKey) {
20
+ const s = get(sessionKey);
21
+ if (s.done) return false;
22
+ if (s.consecErr >= maxErr) return false;
23
+ if (s.turns >= ceiling) return false;
24
+ return true;
25
+ },
26
+ recordOutcome(sessionKey, outcome) {
27
+ const s = get(sessionKey);
28
+ if (outcome === "error") { s.consecErr += 1; return; }
29
+ s.consecErr = 0;
30
+ if (outcome === "applied") s.done = true;
31
+ },
32
+ clear(sessionKey) { byKey.delete(sessionKey); },
33
+ };
34
+ }
35
+
36
+ export default createDistillerBudget;
@@ -0,0 +1,130 @@
1
+ export const DISTILLER_SESSION_PREFIX = "ocuclaw:title-distiller:";
2
+ export const TITLE_MAX = 55;
3
+
4
+ export function isDistillerSessionKey(sessionKey) {
5
+ return typeof sessionKey === "string" && sessionKey.startsWith(DISTILLER_SESSION_PREFIX);
6
+ }
7
+
8
+ // OpenClaw hooks (agent_end et al.) deliver the CANONICAL session key —
9
+ // `agent:<agentId>:<sessionKey>` on 2026.6.x — while relay-side state
10
+ // (first-user marker, title record, user lock) is keyed by the bare relay
11
+ // key the app sends with. Strip exactly one canonical prefix so gate
12
+ // lookups and the distiller recursion guard see the relay key.
13
+ export function stripAgentSessionPrefix(sessionKey) {
14
+ if (typeof sessionKey !== "string") return sessionKey;
15
+ const m = /^agent:[^:]+:(.+)$/.exec(sessionKey);
16
+ return m ? m[1] : sessionKey;
17
+ }
18
+
19
+ export function sanitizeTitle(raw) {
20
+ if (typeof raw !== "string") return null;
21
+ let s = raw.split("\n")[0].trim();
22
+ if (!s) return null;
23
+ // Strip surrounding quotes (straight or curly) and trailing sentence
24
+ // punctuation, repeatedly, so a combination like '"Trip Planning".' fully
25
+ // reduces (a single pass leaves the quote shielded behind the period).
26
+ let prev;
27
+ do {
28
+ prev = s;
29
+ s = s
30
+ .replace(/^["'“”‘’]+/, "")
31
+ .replace(/["'“”‘’]+$/, "")
32
+ .replace(/[.!?,;:]+$/, "")
33
+ .trim();
34
+ } while (s !== prev && s.length > 0);
35
+ if (!s) return null;
36
+ if (s.toUpperCase() === "SKIP") return null;
37
+ if (s.length > TITLE_MAX) s = s.slice(0, TITLE_MAX).trim();
38
+ return s || null;
39
+ }
40
+
41
+ export function internalTranscriptFilename(runId) {
42
+ const safe = String(runId == null ? "" : runId)
43
+ .replace(/[^a-zA-Z0-9._-]/g, "_")
44
+ .slice(0, 120) || "run";
45
+ return `${safe}.jsonl`;
46
+ }
47
+
48
+ function extractText(content) {
49
+ if (typeof content === "string") return content;
50
+ if (!Array.isArray(content)) return "";
51
+ return content
52
+ .filter((b) => b && b.type === "text" && typeof b.text === "string")
53
+ .map((b) => b.text)
54
+ .join("");
55
+ }
56
+
57
+ /**
58
+ * Pull the title text out of a subagent `getSessionMessages` result: the last
59
+ * assistant message's text. Defensive about message shape (string or text-block
60
+ * array content) and about non-assistant trailing entries.
61
+ */
62
+ export function extractAssistantTitleFromMessages(messages) {
63
+ if (!Array.isArray(messages)) return "";
64
+ for (let i = messages.length - 1; i >= 0; i--) {
65
+ const m = messages[i];
66
+ if (!m || typeof m !== "object") continue;
67
+ if (m.role !== "assistant") continue;
68
+ const text = extractText(m.content);
69
+ if (text && text.trim()) return text;
70
+ }
71
+ return "";
72
+ }
73
+
74
+ export function buildExcerpt(messages, opts = {}) {
75
+ const maxMessages = Number.isFinite(opts.maxMessages) ? opts.maxMessages : 6;
76
+ const per = Number.isFinite(opts.perMessageChars) ? opts.perMessageChars : 280;
77
+ const recent = (Array.isArray(messages) ? messages : []).slice(-maxMessages);
78
+ return recent
79
+ .map((m) => {
80
+ const role = m && m.role === "assistant" ? "assistant" : "user";
81
+ let text = extractText(m && m.content).replace(/\s+/g, " ").trim();
82
+ if (text.length > per) text = text.slice(0, per);
83
+ return `${role}: ${text}`;
84
+ })
85
+ .join("\n");
86
+ }
87
+
88
+ /**
89
+ * Split a "provider/model" ref (the sessionTitleModel config format) into the
90
+ * separate provider/model fields the gateway agent RPC expects. Splits on the
91
+ * FIRST "/"; a bare id (or one with an empty half) stays a model-only override.
92
+ * Mirrors the Even-AI model hook's parseModelRef.
93
+ * @returns {{provider?: string, model: string} | null}
94
+ */
95
+ export function splitModelRef(ref) {
96
+ if (typeof ref !== "string") return null;
97
+ const normalized = ref.trim();
98
+ if (!normalized) return null;
99
+ const slash = normalized.indexOf("/");
100
+ if (slash <= 0 || slash >= normalized.length - 1) {
101
+ return { model: normalized };
102
+ }
103
+ const provider = normalized.slice(0, slash).trim();
104
+ const model = normalized.slice(slash + 1).trim();
105
+ if (!provider || !model) return { model: normalized };
106
+ return { provider, model };
107
+ }
108
+
109
+ export function buildDistillerAgentParams(opts) {
110
+ const params = {
111
+ message: opts.message,
112
+ sessionKey: opts.sessionKey,
113
+ idempotencyKey: opts.idempotencyKey,
114
+ modelRun: true,
115
+ promptMode: "none",
116
+ sessionEffects: "internal",
117
+ suppressPromptPersistence: true,
118
+ disableMessageTool: true,
119
+ deliver: false,
120
+ lane: "background",
121
+ };
122
+ // The gateway agent RPC takes provider + model as SEPARATE fields; the config
123
+ // value is "provider/model", so split it rather than sending the slash string.
124
+ const ref = splitModelRef(opts.model);
125
+ if (ref) {
126
+ if (ref.provider) params.provider = ref.provider;
127
+ params.model = ref.model;
128
+ }
129
+ return params;
130
+ }