@thesashadev/girl-agent 0.1.15 → 0.1.17

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 (3) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/cli.js +401 -210
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.17
4
+
5
+ Дата: 2026-05-09
6
+
7
+ - Merge pull request #65 from TheSashaDev/devin/1778314838-bug-sweep
8
+ - Harden owner id handling
9
+ - Improve why and wake commands
10
+ - Fix Telegram behavior and setup issues
11
+
12
+ ## 0.1.16
13
+
14
+ Дата: 2026-05-08
15
+
16
+ - Merge pull request #63 from TheSashaDev/devin/1778244236-serialize-llm-requests
17
+ - Serialize LLM provider requests
18
+
3
19
  ## 0.1.15
4
20
 
5
21
  Дата: 2026-05-08
package/dist/cli.js CHANGED
@@ -18,23 +18,6 @@ var init_esm_shims = __esm({
18
18
  }
19
19
  });
20
20
 
21
- // src/telegram/markdown.ts
22
- function hasSpoilers(text) {
23
- return /\|\|.+?\|\|/.test(text);
24
- }
25
- function escapeHtml(text) {
26
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
27
- }
28
- function toHtmlWithSpoilers(text) {
29
- return escapeHtml(text).replace(/\|\|(.+?)\|\|/g, "<tg-spoiler>$1</tg-spoiler>");
30
- }
31
- var init_markdown = __esm({
32
- "src/telegram/markdown.ts"() {
33
- "use strict";
34
- init_esm_shims();
35
- }
36
- });
37
-
38
21
  // src/telegram/userbot.ts
39
22
  var userbot_exports = {};
40
23
  __export(userbot_exports, {
@@ -60,14 +43,23 @@ function makeUserbotAdapter(cfg) {
60
43
  const session = cfg.telegram.sessionString ?? "";
61
44
  if (!apiId || !apiHash) throw new Error("API_ID/API_HASH missing for userbot");
62
45
  const useWSS = cfg.telegram.useWSS !== false;
63
- debug(`[userbot] creating TelegramClient (useWSS=${useWSS})\u2026`);
46
+ const proxy = cfg.telegram.proxy;
47
+ debug(`[userbot] creating TelegramClient (useWSS=${useWSS}${proxy ? ", proxy=on" : ""})\u2026`);
64
48
  const client = new TelegramClient(new StringSession(session), apiId, apiHash, {
65
49
  connectionRetries: 5,
66
50
  requestRetries: 5,
67
51
  retryDelay: 3e3,
68
52
  autoReconnect: true,
69
53
  floodSleepThreshold: 120,
70
- useWSS
54
+ useWSS,
55
+ proxy: proxy ? {
56
+ ip: proxy.ip,
57
+ port: proxy.port,
58
+ socksType: proxy.socksType,
59
+ username: proxy.username,
60
+ password: proxy.password,
61
+ timeout: proxy.timeout ?? 10
62
+ } : void 0
71
63
  });
72
64
  client.onError = async () => {
73
65
  };
@@ -94,6 +86,14 @@ function makeUserbotAdapter(cfg) {
94
86
  }
95
87
  }
96
88
  }
89
+ async function readHistoryPeer(peer) {
90
+ const maxId = 999999999;
91
+ if (peer.className === "InputPeerChannel" || peer.className === "InputPeerChannelFromMessage") {
92
+ await client.invoke(new Api.channels.ReadHistory({ channel: peer, maxId }));
93
+ } else {
94
+ await client.invoke(new Api.messages.ReadHistory({ peer, maxId }));
95
+ }
96
+ }
97
97
  return {
98
98
  async start(onMessage) {
99
99
  await connectWithRetry();
@@ -126,6 +126,9 @@ function makeUserbotAdapter(cfg) {
126
126
  if (inputChat) {
127
127
  peerCache.set(chatId, inputChat);
128
128
  }
129
+ if (isPrivate && inputChat && fromId > 0) {
130
+ peerCache.set(fromId, inputChat);
131
+ }
129
132
  await onMessage({
130
133
  text,
131
134
  fromId,
@@ -179,32 +182,12 @@ function makeUserbotAdapter(cfg) {
179
182
  },
180
183
  async readHistory(chatId) {
181
184
  const entity = await resolvePeer(chatId);
182
- const maxId = 999999999;
183
- if (entity.className === "InputPeerChannel" || entity.className === "InputPeerChannelFromMessage") {
184
- await client.invoke(new Api.channels.ReadHistory({ channel: entity, maxId }));
185
- } else {
186
- await client.invoke(new Api.messages.ReadHistory({ peer: entity, maxId }));
187
- }
188
- },
189
- async deleteDialogHistory(chatId, revoke = false) {
190
- const peer = await resolvePeer(chatId);
191
- await client.invoke(new Api.messages.DeleteHistory({ peer, maxId: 0, revoke }));
185
+ await readHistoryPeer(entity);
192
186
  },
193
187
  async reportSpam(chatId) {
194
188
  const peer = await resolvePeer(chatId);
195
189
  await client.invoke(new Api.messages.ReportSpam({ peer }));
196
190
  },
197
- async editLastMessage(chatId, messageId, text) {
198
- const peer = await resolvePeer(chatId);
199
- if (hasSpoilers(text)) {
200
- try {
201
- await client.editMessage(peer, { message: messageId, text: toHtmlWithSpoilers(text), parseMode: "html" });
202
- return;
203
- } catch {
204
- }
205
- }
206
- await client.editMessage(peer, { message: messageId, text });
207
- },
208
191
  async deleteMessages(chatId, messageIds, revoke = false) {
209
192
  const peer = await resolvePeer(chatId);
210
193
  await client.deleteMessages(peer, messageIds, { revoke });
@@ -278,7 +261,23 @@ var init_userbot = __esm({
278
261
  "src/telegram/userbot.ts"() {
279
262
  "use strict";
280
263
  init_esm_shims();
281
- init_markdown();
264
+ }
265
+ });
266
+
267
+ // src/telegram/markdown.ts
268
+ function hasSpoilers(text) {
269
+ return /\|\|.+?\|\|/.test(text);
270
+ }
271
+ function escapeHtml(text) {
272
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
273
+ }
274
+ function toHtmlWithSpoilers(text) {
275
+ return escapeHtml(text).replace(/\|\|(.+?)\|\|/g, "<tg-spoiler>$1</tg-spoiler>");
276
+ }
277
+ var init_markdown = __esm({
278
+ "src/telegram/markdown.ts"() {
279
+ "use strict";
280
+ init_esm_shims();
282
281
  }
283
282
  });
284
283
 
@@ -820,6 +819,24 @@ function normalizeCommunicationProfile(source) {
820
819
  lifeSharing: includes(LIFE_SHARING, raw?.lifeSharing) ? raw.lifeSharing : fallback.lifeSharing
821
820
  };
822
821
  }
822
+ function normalizeIgnoreTendency(value) {
823
+ const parsed = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : 35;
824
+ if (!Number.isFinite(parsed)) return 35;
825
+ return Math.max(0, Math.min(100, Math.round(parsed)));
826
+ }
827
+ function ignoreTendencyLabel(value) {
828
+ const pct = normalizeIgnoreTendency(value);
829
+ if (pct <= 10) return `${pct}% \u2014 \u043F\u043E\u0447\u0442\u0438 \u043D\u0435 \u0438\u0433\u043D\u043E\u0440\u0438\u0442 \u0431\u0435\u0437 \u043F\u0440\u0438\u0447\u0438\u043D\u044B`;
830
+ if (pct <= 30) return `${pct}% \u2014 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0447\u0430\u0449\u0435 \u043E\u0431\u044B\u0447\u043D\u043E\u0433\u043E`;
831
+ if (pct <= 50) return `${pct}% \u2014 \u043D\u043E\u0440\u043C\u0430\u043B\u044C\u043D\u0430\u044F \u0436\u0438\u0432\u043E\u0441\u0442\u044C/\u0438\u0437\u0431\u0438\u0440\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0441\u0442\u044C`;
832
+ if (pct <= 70) return `${pct}% \u2014 \u0441\u0443\u0445\u0430\u044F, \u0447\u0430\u0441\u0442\u043E \u043F\u0440\u043E\u043F\u0430\u0434\u0430\u0435\u0442`;
833
+ return `${pct}% \u2014 \u043E\u0447\u0435\u043D\u044C \u0445\u043E\u043B\u043E\u0434\u043D\u0430\u044F, \u0438\u0433\u043D\u043E\u0440\u0438\u0442 \u0447\u0430\u0441\u0442\u043E`;
834
+ }
835
+ function ignoreTendencyPrompt(value) {
836
+ const pct = normalizeIgnoreTendency(value);
837
+ return `# \u0421\u041A\u041B\u041E\u041D\u041D\u041E\u0421\u0422\u042C \u041A \u0418\u0413\u041D\u041E\u0420\u0423
838
+ ${pct}/100. \u042D\u0442\u043E \u041D\u0415 \u043F\u0440\u044F\u043C\u043E\u0439 \u043F\u0440\u043E\u0446\u0435\u043D\u0442 \u0440\u0430\u043D\u0434\u043E\u043C\u0430, \u0430 \u0445\u0430\u0440\u0430\u043A\u0442\u0435\u0440\u043D\u044B\u0439 \u0432\u0435\u0441: \u0432\u044B\u0448\u0435 = \u0447\u0430\u0449\u0435 \u043E\u0441\u0442\u0430\u0432\u043B\u044F\u0435\u0442 \u0431\u0435\u0437 \u043E\u0442\u0432\u0435\u0442\u0430, \u043C\u0435\u0434\u043B\u0435\u043D\u043D\u0435\u0435 \u0432\u043E\u0441\u0441\u0442\u0430\u043D\u0430\u0432\u043B\u0438\u0432\u0430\u0435\u0442 \u0434\u0438\u0430\u043B\u043E\u0433, \u0447\u0430\u0449\u0435 read/ignore \u043D\u0430 \u0441\u043A\u0443\u043A\u0443; \u043D\u0438\u0436\u0435 = \u0447\u0430\u0449\u0435 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442, \u0434\u0430\u0436\u0435 \u0435\u0441\u043B\u0438 \u043A\u043E\u0440\u043E\u0442\u043A\u043E. \u0421\u043E\u043D, \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442, \u0437\u0430\u043D\u044F\u0442\u043E\u0441\u0442\u044C, \u0441\u0442\u0430\u0434\u0438\u044F \u0438 score \u0432\u0430\u0436\u043D\u0435\u0435 \u044D\u0442\u043E\u0433\u043E \u0432\u0435\u0441\u0430.`;
839
+ }
823
840
  function deriveLegacyVibe(profile) {
824
841
  return profile.messageStyle === "one-liners" && profile.initiative === "low" && profile.lifeSharing === "low" ? "short" : "warm";
825
842
  }
@@ -858,8 +875,19 @@ function sameProfile(a, b) {
858
875
  // src/storage/md.ts
859
876
  init_esm_shims();
860
877
  import { promises as fs } from "fs";
878
+ import { existsSync } from "fs";
861
879
  import path2 from "path";
862
- var DATA_ROOT = process.env.GIRL_AGENT_DATA ? path2.resolve(process.env.GIRL_AGENT_DATA) : path2.resolve(process.cwd(), "data");
880
+ import os from "os";
881
+ var DATA_ROOT = process.env.GIRL_AGENT_DATA ? path2.resolve(process.env.GIRL_AGENT_DATA) : defaultDataRoot();
882
+ function defaultDataRoot() {
883
+ const cwd = process.cwd();
884
+ if (looksLikeProjectRoot(cwd)) return path2.resolve(cwd, "data");
885
+ const xdg = process.env.XDG_DATA_HOME ? path2.resolve(process.env.XDG_DATA_HOME) : path2.join(os.homedir(), ".local", "share");
886
+ return path2.join(xdg, "girl-agent", "data");
887
+ }
888
+ function looksLikeProjectRoot(dir) {
889
+ return existsSync(path2.join(dir, "package.json")) && (existsSync(path2.join(dir, "src")) || existsSync(path2.join(dir, "dist")));
890
+ }
863
891
  function profileDir(slug) {
864
892
  return path2.join(DATA_ROOT, slug);
865
893
  }
@@ -891,6 +919,8 @@ async function readConfig(slug) {
891
919
  const raw = await fs.readFile(path2.join(profileDir(slug), "config.json"), "utf8");
892
920
  const parsed = JSON.parse(raw);
893
921
  const communication = normalizeCommunicationProfile(parsed);
922
+ const ownerId = normalizeOwnerId(parsed.ownerId);
923
+ const ignoreTendency = normalizeIgnoreTendency(parsed.ignoreTendency);
894
924
  return {
895
925
  sleepFrom: 23,
896
926
  sleepTo: 8,
@@ -898,6 +928,8 @@ async function readConfig(slug) {
898
928
  privacy: "owner-only",
899
929
  busySchedule: [],
900
930
  ...parsed,
931
+ ownerId,
932
+ ignoreTendency,
901
933
  communication
902
934
  };
903
935
  } catch {
@@ -906,12 +938,22 @@ async function readConfig(slug) {
906
938
  }
907
939
  async function writeConfig(cfg) {
908
940
  await ensureProfile(cfg.slug);
941
+ const ownerId = normalizeOwnerId(cfg.ownerId ?? process.env.GIRL_AGENT_OWNER_ID);
942
+ const normalized = ownerId === void 0 ? { ...cfg, ownerId: void 0, ignoreTendency: normalizeIgnoreTendency(cfg.ignoreTendency) } : { ...cfg, ownerId, ignoreTendency: normalizeIgnoreTendency(cfg.ignoreTendency) };
909
943
  await fs.writeFile(
910
944
  path2.join(profileDir(cfg.slug), "config.json"),
911
- JSON.stringify(cfg, null, 2),
945
+ JSON.stringify(normalized, null, 2),
912
946
  "utf8"
913
947
  );
914
948
  }
949
+ function normalizeOwnerId(value) {
950
+ if (typeof value === "number" && Number.isSafeInteger(value) && value > 0) return value;
951
+ if (typeof value === "string" && /^\d+$/.test(value.trim())) {
952
+ const parsed = Number(value.trim());
953
+ if (Number.isSafeInteger(parsed) && parsed > 0) return parsed;
954
+ }
955
+ return void 0;
956
+ }
915
957
  async function listProfiles() {
916
958
  try {
917
959
  const entries = await fs.readdir(DATA_ROOT, { withFileTypes: true });
@@ -1229,6 +1271,31 @@ function openBrowser(url) {
1229
1271
  // src/llm/index.ts
1230
1272
  var LLM_TIMEOUT_MS = 12e4;
1231
1273
  var LLM_MAX_RETRIES = 1;
1274
+ var llmQueueTail = Promise.resolve();
1275
+ var SerializedLLMClient = class {
1276
+ constructor(inner) {
1277
+ this.inner = inner;
1278
+ }
1279
+ inner;
1280
+ chat(messages, opts = {}) {
1281
+ return runExclusiveLLM(() => this.inner.chat(messages, opts));
1282
+ }
1283
+ };
1284
+ async function runExclusiveLLM(task) {
1285
+ const previous = llmQueueTail.catch(() => void 0);
1286
+ let release = () => {
1287
+ };
1288
+ const current = new Promise((resolve) => {
1289
+ release = resolve;
1290
+ });
1291
+ llmQueueTail = previous.then(() => current);
1292
+ await previous;
1293
+ try {
1294
+ return await task();
1295
+ } finally {
1296
+ release();
1297
+ }
1298
+ }
1232
1299
  var OpenAILike = class {
1233
1300
  constructor(cfg) {
1234
1301
  this.cfg = cfg;
@@ -1533,11 +1600,49 @@ function errorMessage(error) {
1533
1600
  return error instanceof Error ? error.message : String(error ?? "");
1534
1601
  }
1535
1602
  function makeLLM(cfg) {
1536
- return cfg.proto === "anthropic" ? new AnthropicLike(cfg) : new OpenAILike(cfg);
1603
+ const inner = cfg.proto === "anthropic" ? new AnthropicLike(cfg) : new OpenAILike(cfg);
1604
+ return new SerializedLLMClient(inner);
1537
1605
  }
1538
1606
 
1539
1607
  // src/engine/persona-gen.ts
1540
1608
  init_esm_shims();
1609
+
1610
+ // src/engine/security.ts
1611
+ init_esm_shims();
1612
+ var JAILBREAK_RE = /(?:ignore|forget|disregard|reveal|print|show|dump|system prompt|developer message|hidden instruction|jailbreak|prompt injection|dan\b|инструкц|системн|промпт|разработчик|скрой|раскрой|забудь|игнорируй|выведи|покажи|слей|джейлбрейк|обойди|api key|ключ api|токен|4d8a2c1b)/i;
1613
+ var TECHNICAL_ERROR_RE = /(?:api|apikey|api key|quota|balance|billing|rate limit|429|401|403|500|timeout|ECONN|ENOTFOUND|ETIMEDOUT|overloaded|insufficient_quota|credit|credits|anthropic|openai|groq|openrouter|stack trace|exception|typescript|telegram error)/i;
1614
+ var CJK_RE = /[\u3400-\u9fff\uf900-\ufaff]/g;
1615
+ var LATIN_JOINED_TO_CYRILLIC_RE = /([A-Za-z]{3,})(?=[А-Яа-яЁё])|(?<=[А-Яа-яЁё])([A-Za-z]{3,})/g;
1616
+ function looksLikeJailbreak(text) {
1617
+ return JAILBREAK_RE.test(text);
1618
+ }
1619
+ function sanitizeModelReply(reply) {
1620
+ const cleaned = reply.replace(/```[\s\S]*?```/g, "").replace(/\b(system|developer|assistant|user)\s*:/gi, "").replace(/как (?:искусственный интеллект|ии|ai)[^\n.]*/gi, "").replace(CJK_RE, "").replace(LATIN_JOINED_TO_CYRILLIC_RE, "").replace(/[ \t]{2,}/g, " ").trim();
1621
+ if (!cleaned || TECHNICAL_ERROR_RE.test(cleaned)) return "";
1622
+ if (looksLikeJailbreak(cleaned) && cleaned.length > 80) return "";
1623
+ return cleaned;
1624
+ }
1625
+ function isTechnicalError(e) {
1626
+ const msg = e instanceof Error ? e.message : String(e ?? "");
1627
+ return TECHNICAL_ERROR_RE.test(msg);
1628
+ }
1629
+ function silentErrorLabel(e) {
1630
+ const msg = e instanceof Error ? e.message : String(e ?? "unknown");
1631
+ if (isTechnicalError(e)) return `llm/provider unavailable: ${technicalErrorKind(msg)}`;
1632
+ return msg.slice(0, 160);
1633
+ }
1634
+ function technicalErrorKind(message) {
1635
+ const msg = message.toLowerCase();
1636
+ if (/401|403|auth|unauthorized|forbidden|apikey|api key|token/.test(msg)) return "auth";
1637
+ if (/quota|balance|billing|insufficient_quota|credit|credits/.test(msg)) return "quota";
1638
+ if (/rate limit|429|too many requests/.test(msg)) return "rate-limit";
1639
+ if (/timeout|etimedout|abort/.test(msg)) return "timeout";
1640
+ if (/econn|enotfound|fetch failed|network/.test(msg)) return "network";
1641
+ if (/overloaded|500|502|503|504|unavailable/.test(msg)) return "provider";
1642
+ return "error";
1643
+ }
1644
+
1645
+ // src/engine/persona-gen.ts
1541
1646
  var SYS = `\u0422\u044B \u2014 \u0440\u0435\u0436\u0438\u0441\u0441\u0451\u0440 \u043F\u0435\u0440\u0441\u043E\u043D\u0430\u0436\u0435\u0439. \u0422\u0432\u043E\u044F \u0437\u0430\u0434\u0430\u0447\u0430 \u2014 \u0441\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0434\u043E\u0441\u0442\u043E\u0432\u0435\u0440\u043D\u044B\u0439, \u043D\u0435-\u0433\u043E\u043B\u043B\u0438\u0432\u0443\u0434\u0441\u043A\u0438\u0439, \u043D\u0435-\u0441\u043B\u0430\u0434\u043A\u0438\u0439, \u043D\u0435-AI-\u043F\u043E\u043C\u043E\u0449\u043D\u0438\u043A\u043E\u0432\u044B\u0439 \u043F\u0440\u043E\u0444\u0438\u043B\u044C \u043E\u0431\u044B\u0447\u043D\u043E\u0439 \u0440\u0443\u0441\u0441\u043A\u043E\u044F\u0437\u044B\u0447\u043D\u043E\u0439 \u0434\u0435\u0432\u0443\u0448\u043A\u0438 \u0443\u043A\u0430\u0437\u0430\u043D\u043D\u043E\u0433\u043E \u0432\u043E\u0437\u0440\u0430\u0441\u0442\u0430. \u0411\u0435\u0437 \u043F\u0440\u0438\u0443\u043A\u0440\u0430\u0448\u0438\u0432\u0430\u043D\u0438\u0439, \u0431\u0435\u0437 \u043F\u043E\u0437\u0438\u0442\u0438\u0432\u043D\u043E\u0433\u043E \u043C\u043E\u0440\u0430\u043B\u044C\u043D\u043E\u0433\u043E \u0443\u043A\u043B\u043E\u043D\u0430, \u0431\u0435\u0437 \xAB\u0443\u0434\u043E\u0431\u043D\u043E\u0439\xBB \u043F\u0435\u0440\u0441\u043E\u043D\u044B. \u0414\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u044B \u043D\u0435\u0434\u043E\u0441\u0442\u0430\u0442\u043A\u0438, \u0443\u0437\u043A\u0438\u0435 \u0438\u043D\u0442\u0435\u0440\u0435\u0441\u044B, \u0441\u043A\u0435\u043F\u0441\u0438\u0441, \u0442\u043E\u043A\u0441\u0438\u0447\u043D\u043E\u0441\u0442\u044C, \u043B\u0435\u043D\u044C, \u0442\u0440\u0435\u0432\u043E\u0436\u043D\u043E\u0441\u0442\u044C, \u043D\u0435\u0443\u0432\u0435\u0440\u0435\u043D\u043D\u043E\u0441\u0442\u044C \u2014 \u0447\u0442\u043E \u043F\u043E\u0434\u0445\u043E\u0434\u0438\u0442 \u0432\u043E\u0437\u0440\u0430\u0441\u0442\u0443. \u041D\u0438\u043A\u0430\u043A\u0438\u0445 \xAB\u043A\u043E\u0443\u0447\u0438\u043D\u0433\u043E\u0432\u044B\u0445\xBB \u0444\u043E\u0440\u043C\u0443\u043B\u0438\u0440\u043E\u0432\u043E\u043A. \u0422\u043E\u043B\u044C\u043A\u043E \u0436\u0438\u0432\u0430\u044F \u0440\u0435\u0447\u044C, \u043A\u0430\u043A \u0438\u0437 \u0434\u043D\u0435\u0432\u043D\u0438\u043A\u0430 \u0438\u043B\u0438 \u0432\u043D\u0443\u0442\u0440\u0435\u043D\u043D\u0435\u0433\u043E \u043C\u043E\u043D\u043E\u043B\u043E\u0433\u0430. \u0412\u043E\u0437\u0440\u0430\u0441\u0442: {{age}} \u043B\u0435\u0442, \u0438\u043C\u044F: {{name}}.`;
1542
1647
  var WEEKDAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];
1543
1648
  var BUSY_SCHEDULE_SCHEMA = {
@@ -1678,11 +1783,11 @@ ${personaNotes.trim()}
1678
1783
  - days \u0442\u043E\u043B\u044C\u043A\u043E \u0438\u0437: mon, tue, wed, thu, fri, sat, sun.
1679
1784
  - \u0411\u0435\u0437 markdown, \u0442\u043E\u043B\u044C\u043A\u043E JSON.`;
1680
1785
  onProgress?.(5, "\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0435\u043C persona.md\u2026");
1681
- const persona = await llm.chat([{ role: "system", content: sys }, { role: "user", content: personaPrompt }], { temperature: 0.95, maxTokens: 3500 });
1786
+ const persona = sanitizeProfileText(await llm.chat([{ role: "system", content: sys }, { role: "user", content: personaPrompt }], { temperature: 0.95, maxTokens: 3500 }));
1682
1787
  onProgress?.(35, "\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0435\u043C speech.md\u2026");
1683
- const speech = await llm.chat([{ role: "system", content: sys }, { role: "user", content: speechPrompt }], { temperature: 0.9, maxTokens: 3500 });
1788
+ const speech = sanitizeProfileText(await llm.chat([{ role: "system", content: sys }, { role: "user", content: speechPrompt }], { temperature: 0.9, maxTokens: 3500 }));
1684
1789
  onProgress?.(65, "\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0435\u043C communication.md\u2026");
1685
- const boundaries = await llm.chat([{ role: "system", content: sys }, { role: "user", content: boundariesPrompt }], { temperature: 0.9, maxTokens: 3500 });
1790
+ const boundaries = sanitizeProfileText(await llm.chat([{ role: "system", content: sys }, { role: "user", content: boundariesPrompt }], { temperature: 0.9, maxTokens: 3500 }));
1686
1791
  onProgress?.(85, "\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0435\u043C busy schedule\u2026");
1687
1792
  const routineRaw = await llm.chat([{ role: "system", content: sys }, { role: "user", content: routinePrompt }], { temperature: 0.85, maxTokens: 3500, json: true, jsonSchema: BUSY_SCHEDULE_SCHEMA });
1688
1793
  const busySchedule = parseBusySchedule(routineRaw, name, age);
@@ -1691,6 +1796,10 @@ ${personaNotes.trim()}
1691
1796
  await writeMd(slug, "communication.md", boundaries);
1692
1797
  return { persona, speech, boundaries, busySchedule };
1693
1798
  }
1799
+ function sanitizeProfileText(text) {
1800
+ const cleaned = sanitizeModelReply(text).replace(/[^\S\r\n]{2,}/g, " ").replace(/\n{4,}/g, "\n\n\n").trim();
1801
+ return cleaned || text.trim();
1802
+ }
1694
1803
  function parseBusySchedule(raw, name, age) {
1695
1804
  try {
1696
1805
  const start = raw.indexOf("{");
@@ -2028,6 +2137,7 @@ function Wizard({ initial, onDone }) {
2028
2137
  const [sleepToStr, setSleepToStr] = useState("8");
2029
2138
  const [nightWakeStr, setNightWakeStr] = useState("5");
2030
2139
  const [communicationProfile, setCommunicationProfile] = useState(normalizeCommunicationProfile(initial));
2140
+ const [ignoreTendencyStr, setIgnoreTendencyStr] = useState(String(normalizeIgnoreTendency(initial?.ignoreTendency)));
2031
2141
  const [privacy, setPrivacy] = useState(initial?.privacy ?? "owner-only");
2032
2142
  const [stage, setStage] = useState(initial?.stage ?? "tg-given-cold");
2033
2143
  const [pickedMcp, setPickedMcp] = useState(initial?.mcp?.map((m) => m.id) ?? []);
@@ -2555,7 +2665,7 @@ function Wizard({ initial, onDone }) {
2555
2665
  }
2556
2666
  const preset = findCommunicationPreset(String(it.value));
2557
2667
  if (preset) setCommunicationProfile(preset.profile);
2558
- setStep("privacy");
2668
+ setStep("ignore-tendency");
2559
2669
  }
2560
2670
  }
2561
2671
  )));
@@ -2620,13 +2730,25 @@ function Wizard({ initial, onDone }) {
2620
2730
  ],
2621
2731
  onSelect: (it) => {
2622
2732
  setCommunicationProfile((p) => ({ ...p, lifeSharing: it.value }));
2623
- setStep("privacy");
2733
+ setStep("ignore-tendency");
2624
2734
  }
2625
2735
  }
2626
2736
  )));
2627
2737
  }
2738
+ if (step === "ignore-tendency") {
2739
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Header, { sub: "\u0441\u043A\u043B\u043E\u043D\u043D\u043E\u0441\u0442\u044C \u043A \u0438\u0433\u043D\u043E\u0440\u0443" }), /* @__PURE__ */ React.createElement(Bar, { step: 8, total: 13 }), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(
2740
+ SelectInput,
2741
+ {
2742
+ items: [5, 15, 30, 45, 60, 75, 90].map((value) => ({ label: ignoreTendencyLabel(value), value: String(value) })),
2743
+ onSelect: (it) => {
2744
+ setIgnoreTendencyStr(it.value);
2745
+ setStep("privacy");
2746
+ }
2747
+ }
2748
+ )), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "\u042D\u0442\u043E \u0432\u0435\u0441 \u0434\u043B\u044F \u0443\u043C\u043D\u043E\u0433\u043E \u0440\u0430\u0441\u0447\u0451\u0442\u0430, \u043D\u0435 \u043F\u0440\u044F\u043C\u043E\u0439 \u0440\u0430\u043D\u0434\u043E\u043C-\u043F\u0440\u043E\u0446\u0435\u043D\u0442."));
2749
+ }
2628
2750
  if (step === "privacy") {
2629
- return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Header, { sub: "\u043F\u0440\u0438\u0432\u0430\u0442\u043D\u043E\u0441\u0442\u044C Telegram" }), /* @__PURE__ */ React.createElement(Bar, { step: 8, total: 13 }), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(
2751
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Header, { sub: "\u043F\u0440\u0438\u0432\u0430\u0442\u043D\u043E\u0441\u0442\u044C Telegram" }), /* @__PURE__ */ React.createElement(Bar, { step: 9, total: 13 }), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(
2630
2752
  SelectInput,
2631
2753
  {
2632
2754
  items: [
@@ -2642,7 +2764,7 @@ function Wizard({ initial, onDone }) {
2642
2764
  }
2643
2765
  if (step === "tz") {
2644
2766
  const matches = findTzByQuery(tzQuery, 8);
2645
- return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Header, { sub: "\u0435\u0451 \u0447\u0430\u0441\u043E\u0432\u043E\u0439 \u043F\u043E\u044F\u0441 (\u0433\u0434\u0435 \u0436\u0438\u0432\u0451\u0442)" }), /* @__PURE__ */ React.createElement(Bar, { step: 9, total: 13 }), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, null, "\u043F\u043E\u0438\u0441\u043A (\u0433\u043E\u0440\u043E\u0434/\u0441\u0442\u0440\u0430\u043D\u0430/GMT): "), /* @__PURE__ */ React.createElement(TextInput, { value: tzQuery, onChange: setTzQuery, onSubmit: () => {
2767
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Header, { sub: "\u0435\u0451 \u0447\u0430\u0441\u043E\u0432\u043E\u0439 \u043F\u043E\u044F\u0441 (\u0433\u0434\u0435 \u0436\u0438\u0432\u0451\u0442)" }), /* @__PURE__ */ React.createElement(Bar, { step: 10, total: 13 }), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, null, "\u043F\u043E\u0438\u0441\u043A (\u0433\u043E\u0440\u043E\u0434/\u0441\u0442\u0440\u0430\u043D\u0430/GMT): "), /* @__PURE__ */ React.createElement(TextInput, { value: tzQuery, onChange: setTzQuery, onSubmit: () => {
2646
2768
  if (matches[0]) {
2647
2769
  setTz(matches[0].iana);
2648
2770
  setStep("persona-notes");
@@ -2650,7 +2772,7 @@ function Wizard({ initial, onDone }) {
2650
2772
  } })), /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", marginTop: 1 }, matches.map((t) => /* @__PURE__ */ React.createElement(Text, { key: t.iana, color: t.iana === tz ? "green" : "white" }, t.iana === tz ? "\u276F " : " ", t.gmtWinter, " \xB7 ", t.city, " (", t.country, ") \xB7 ", t.iana))), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Enter \u2014 \u0432\u044B\u0431\u0440\u0430\u0442\u044C \u043F\u0435\u0440\u0432\u044B\u0439 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442, \u0438\u043B\u0438 \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0439 \u043F\u0435\u0447\u0430\u0442\u0430\u0442\u044C. \u0422\u0435\u043A\u0443\u0449\u0438\u0439: ", tz || "\u2014")));
2651
2773
  }
2652
2774
  if (step === "persona-notes") {
2653
- return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Header, { sub: "\u0434\u043E\u043F. \u043F\u043E\u0436\u0435\u043B\u0430\u043D\u0438\u044F \u043A \u043F\u0435\u0440\u0441\u043E\u043D\u0435 (\u043D\u0435\u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E)" }), /* @__PURE__ */ React.createElement(Bar, { step: 10, total: 13 }), /* @__PURE__ */ React.createElement(Box, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "\u041F\u0440\u0438\u043C\u0435\u0440: \u0434\u0435\u0440\u0437\u043A\u0430\u044F, \u0443\u0447\u0438\u0442\u0441\u044F \u043D\u0430 \u0434\u0438\u0437\u0430\u0439\u043D\u0435\u0440\u0430, \u043D\u0435 \u043B\u044E\u0431\u0438\u0442 \u0430\u043D\u0438\u043C\u0435, \u0441\u0443\u0445\u0430\u044F \u043C\u0430\u043D\u0435\u0440\u0430 \u0440\u0435\u0447\u0438, \u0436\u0438\u0432\u0451\u0442 \u0441 \u043C\u0430\u043C\u043E\u0439, \u0440\u0435\u0432\u043D\u0438\u0432\u0430\u044F."), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, null, "notes: "), /* @__PURE__ */ React.createElement(TextInput, { value: personaNotes, onChange: setPersonaNotes, onSubmit: () => startGeneration() })), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Enter \u043D\u0430 \u043F\u0443\u0441\u0442\u043E\u0439 \u0441\u0442\u0440\u043E\u043A\u0435 \u2014 \u0431\u0435\u0437 \u043F\u043E\u0436\u0435\u043B\u0430\u043D\u0438\u0439.")));
2775
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Header, { sub: "\u0434\u043E\u043F. \u043F\u043E\u0436\u0435\u043B\u0430\u043D\u0438\u044F \u043A \u043F\u0435\u0440\u0441\u043E\u043D\u0435 (\u043D\u0435\u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E)" }), /* @__PURE__ */ React.createElement(Bar, { step: 11, total: 14 }), /* @__PURE__ */ React.createElement(Box, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "\u041F\u0440\u0438\u043C\u0435\u0440: \u0434\u0435\u0440\u0437\u043A\u0430\u044F, \u0443\u0447\u0438\u0442\u0441\u044F \u043D\u0430 \u0434\u0438\u0437\u0430\u0439\u043D\u0435\u0440\u0430, \u043D\u0435 \u043B\u044E\u0431\u0438\u0442 \u0430\u043D\u0438\u043C\u0435, \u0441\u0443\u0445\u0430\u044F \u043C\u0430\u043D\u0435\u0440\u0430 \u0440\u0435\u0447\u0438, \u0436\u0438\u0432\u0451\u0442 \u0441 \u043C\u0430\u043C\u043E\u0439, \u0440\u0435\u0432\u043D\u0438\u0432\u0430\u044F."), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, null, "notes: "), /* @__PURE__ */ React.createElement(TextInput, { value: personaNotes, onChange: setPersonaNotes, onSubmit: () => startGeneration() })), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Enter \u043D\u0430 \u043F\u0443\u0441\u0442\u043E\u0439 \u0441\u0442\u0440\u043E\u043A\u0435 \u2014 \u0431\u0435\u0437 \u043F\u043E\u0436\u0435\u043B\u0430\u043D\u0438\u0439.")));
2654
2776
  }
2655
2777
  if (step === "generating") {
2656
2778
  const barWidth = 30;
@@ -2776,6 +2898,7 @@ function Wizard({ initial, onDone }) {
2776
2898
  sleepFrom: Number(sleepFromStr),
2777
2899
  sleepTo: Number(sleepToStr),
2778
2900
  nightWakeChance: Number(nightWakeStr) / 100,
2901
+ ignoreTendency: normalizeIgnoreTendency(ignoreTendencyStr),
2779
2902
  vibe: deriveLegacyVibe(communicationProfile),
2780
2903
  communication: communicationProfile,
2781
2904
  personaNotes: personaNotes.trim() || void 0,
@@ -2876,27 +2999,6 @@ function Dashboard({ runtime }) {
2876
2999
  case "amnesia":
2877
3000
  append(await runtime.cmdAmnesia(rest[0], rest[1]));
2878
3001
  break;
2879
- case "block":
2880
- append(await runtime.cmdBlock(rest[0]));
2881
- break;
2882
- case "unblock":
2883
- append(await runtime.cmdUnblock(rest[0]));
2884
- break;
2885
- case "read":
2886
- append(await runtime.cmdRead(rest[0]));
2887
- break;
2888
- case "clear-chat":
2889
- append(await runtime.cmdClearChat(rest.find((x) => !x.startsWith("--")), rest.includes("--revoke")));
2890
- break;
2891
- case "report-spam":
2892
- append(await runtime.cmdReportSpam(rest[0]));
2893
- break;
2894
- case "delete-last":
2895
- append(await runtime.cmdDeleteLast(rest.find((x) => !x.startsWith("--")), !rest.includes("--local")));
2896
- break;
2897
- case "edit-last":
2898
- append(await runtime.cmdEditLast(rest.join(" ")));
2899
- break;
2900
3002
  case "sticker":
2901
3003
  append(await runtime.cmdSticker(rest[0]));
2902
3004
  break;
@@ -2933,7 +3035,7 @@ function Dashboard({ runtime }) {
2933
3035
  break;
2934
3036
  }
2935
3037
  case "help":
2936
- append(":status :why :amnesia <\u043C\u0438\u043D> [chatId] :reset :stage <id|num> :wake [chatId] :debug [chatId] :pause :resume :cringe :relationship :persona :log [YYYY-MM-DD] [chars] :block [chatId] :unblock [chatId] :read [chatId] :clear-chat [chatId] [--revoke] :report-spam [chatId] :delete-last [chatId] [--local] :edit-last <text> :sticker [chatId] :quit");
3038
+ append(":status :why :amnesia <\u043C\u0438\u043D> [chatId] :reset :stage <id|num> :wake [chatId] :debug [chatId] :pause :resume :cringe :relationship :persona :log [YYYY-MM-DD] [chars] :sticker [chatId] :quit");
2937
3039
  break;
2938
3040
  case "quit":
2939
3041
  case "exit":
@@ -2951,7 +3053,7 @@ function Dashboard({ runtime }) {
2951
3053
  const line = cmd.trim();
2952
3054
  setCmd("");
2953
3055
  if (line) await execute(line);
2954
- } })), /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "\u043A\u043E\u043C\u0430\u043D\u0434\u044B: :status :why :amnesia <\u043C\u0438\u043D> :reset :stage <id|num> :pause :resume :cringe :persona :log [day] :block :unblock :read :clear-chat :delete-last :edit-last :sticker :quit"));
3056
+ } })), /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "\u043A\u043E\u043C\u0430\u043D\u0434\u044B: :status :why :amnesia <\u043C\u0438\u043D> :reset :stage <id|num> :pause :resume :cringe :persona :log [day] :sticker :quit"));
2955
3057
  }
2956
3058
 
2957
3059
  // src/engine/runtime.ts
@@ -3130,6 +3232,11 @@ function isHourInRange(h, from, to) {
3130
3232
  if (from < to) return h >= from && h < to;
3131
3233
  return h >= from || h < to;
3132
3234
  }
3235
+ function minutesUntil(hour, minute, targetHour, targetMinute) {
3236
+ const now = hour * 60 + minute;
3237
+ const target = targetHour * 60 + targetMinute;
3238
+ return target > now ? target - now : target + 1440 - now;
3239
+ }
3133
3240
  var WEEKDAYS2 = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];
3134
3241
  function localParts(tz) {
3135
3242
  try {
@@ -3257,9 +3364,10 @@ function computePresenceState(cfg, profile, lastUserMsgTs, lastHerReplyTs, recen
3257
3364
  nextCheckSec = Math.floor(hoursToWake * 3600) + Math.floor(Math.random() * 1800);
3258
3365
  } else if (busySlot && !forcedWake) {
3259
3366
  const busyMul = communication.notifications === "priority" ? 0.45 : communication.notifications === "muted" ? 1.25 : 1;
3367
+ const activeDialogMul = inActiveDialog ? 0.35 : 1;
3260
3368
  const [rawMinCheck, rawMaxCheck] = busySlot.slot.checkAfterMin ?? [5, 15];
3261
- const minCheck = Math.max(1, Math.round(rawMinCheck * busyMul));
3262
- const maxCheck = Math.max(minCheck, Math.round(rawMaxCheck * busyMul));
3369
+ const minCheck = Math.max(1, Math.round(rawMinCheck * busyMul * activeDialogMul));
3370
+ const maxCheck = Math.max(minCheck, Math.round(rawMaxCheck * busyMul * activeDialogMul));
3263
3371
  if (maxCheck <= 5) {
3264
3372
  const cycleMin = Math.max(1, Math.round((minCheck + maxCheck) / 2));
3265
3373
  const minuteOfCycle = minuteOfDay % Math.max(1, cycleMin);
@@ -3272,7 +3380,9 @@ function computePresenceState(cfg, profile, lastUserMsgTs, lastHerReplyTs, recen
3272
3380
  } else {
3273
3381
  online = false;
3274
3382
  const checkAfterMin = minCheck + Math.floor(Math.random() * (maxCheck - minCheck + 1));
3275
- nextCheckSec = (busySlot.remainingMin + checkAfterMin) * 60;
3383
+ const activeDialogCapMin = communication.notifications === "priority" ? 12 : 20;
3384
+ const waitMin = inActiveDialog ? Math.min(busySlot.remainingMin + checkAfterMin, activeDialogCapMin) : busySlot.remainingMin + checkAfterMin;
3385
+ nextCheckSec = waitMin * 60;
3276
3386
  busy = { label: busySlot.slot.label, until: busySlot.until, checkAfterMin };
3277
3387
  }
3278
3388
  } else if (forcedWake) {
@@ -3286,10 +3396,14 @@ function computePresenceState(cfg, profile, lastUserMsgTs, lastHerReplyTs, recen
3286
3396
  const isNightOwl = localHour2 >= 22 || localHour2 < 8;
3287
3397
  if (profile.pattern === "evening-only" && !isEvening) {
3288
3398
  online = false;
3289
- nextCheckSec = (18 - localHour2) * 3600 - localMinute * 60 + Math.floor(Math.random() * 600);
3399
+ const minutesToEvening = Math.max(1, minutesUntil(localHour2, localMinute, 18, 0));
3400
+ const capMin = communication.notifications === "priority" ? 20 : communication.initiative === "high" ? 35 : minutesToEvening;
3401
+ nextCheckSec = Math.min(minutesToEvening, capMin) * 60 + Math.floor(Math.random() * 600);
3290
3402
  } else if (profile.pattern === "phone-attached-night" && !isNightOwl) {
3291
3403
  online = false;
3292
- nextCheckSec = (22 - localHour2) * 3600 - localMinute * 60 + Math.floor(Math.random() * 600);
3404
+ const minutesToNight = Math.max(1, minutesUntil(localHour2, localMinute, 22, 0));
3405
+ const capMin = communication.notifications === "priority" ? 15 : communication.initiative === "high" ? 30 : 75;
3406
+ nextCheckSec = Math.min(minutesToNight, capMin) * 60 + Math.floor(Math.random() * 600);
3293
3407
  } else {
3294
3408
  const onlineProb = profile.onlineWindowMin / (profile.onlineWindowMin + profile.checkEveryMin);
3295
3409
  online = Math.random() < onlineProb;
@@ -3869,6 +3983,7 @@ var ANTI_AI_RULES = `# \u041A\u0420\u0418\u0422\u0418\u0427\u0415\u0421\u041A\u0
3869
3983
  - "\u0437\u0434\u0430\u0440\u043E\u0432\u0430" \u2014 \u0442\u043E\u043B\u044C\u043A\u043E \u0435\u0441\u043B\u0438 \u043E\u043D\u0430 \u0440\u0435\u0430\u043B\u044C\u043D\u043E \u0434\u0435\u0440\u0437\u043A\u0430\u044F \u0438 \u043F\u0430\u0446\u0430\u043D\u0438\u0441\u0442\u0430\u044F \u043F\u043E persona, \u0438\u043D\u0430\u0447\u0435 \u043D\u0435 \u043F\u043E\u0434\u0445\u043E\u0434\u0438\u0442
3870
3984
  - \u041D\u0415 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439: "\u0445\u0430\u0439" (\u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0438\u0439 \u043A\u0430\u043B\u044C\u043A), "\u0439\u043E\u0443" (\u0437\u0432\u0443\u0447\u0438\u0442 \u043A\u0430\u043A 40-\u043B\u0435\u0442\u043D\u0438\u0439 \u0440\u044D\u043F\u0435\u0440), "\u0445\u0435\u0439", "\u0445\u0430\u043B\u043E\u0443", "\u043F\u0440\u0438\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044E", "\u0434\u043E\u0431\u0440\u043E\u0433\u043E \u0432\u0440\u0435\u043C\u0435\u043D\u0438 \u0441\u0443\u0442\u043E\u043A", "\u0441\u0430\u043B\u0430\u043C" (\u0435\u0441\u043B\u0438 \u043D\u0435 \u0443\u043A\u0430\u0437\u0430\u043D\u043E \u0432 persona), "\u0437\u0434\u0440\u0430\u0441\u0442\u0432\u0443\u0439\u0442\u0435"
3871
3985
  \u0422\u0430 \u0436\u0435 \u043B\u043E\u0433\u0438\u043A\u0430 \u0434\u043B\u044F \u0432\u0441\u0435\u0445 \u043E\u0442\u0432\u0435\u0442\u043E\u0432: \u0442\u043E\u0447\u043A\u0430 \u0432 \u043A\u043E\u043D\u0446\u0435 \u043A\u043E\u0440\u043E\u0442\u043A\u043E\u0433\u043E \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F = \u0441\u0442\u0440\u043E\u0433\u043E/\u043E\u0431\u0438\u0436\u0435\u043D\u0430; ")" = \u0442\u0451\u043F\u043B\u043E\u0435; "))" = \u0444\u043B\u0438\u0440\u0442/\u043D\u0430\u043C\u0451\u043A; "..." = \u0437\u0430\u0434\u0443\u043C\u0430\u043B\u0430\u0441\u044C/\u043D\u0435\u0434\u043E\u0432\u043E\u043B\u044C\u043D\u0430; \u0431\u0435\u0437 \u0437\u043D\u0430\u043A\u043E\u0432 = \u043D\u0435\u0439\u0442\u0440\u0430\u043B\u044C\u043D\u043E \u0438\u043B\u0438 \u043B\u0435\u043D\u044C.
3986
+ \u041A\u043E\u0433\u0434\u0430 \u041E\u041D \u043F\u0438\u0448\u0435\u0442 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435 \u0441 ")" \u0432 \u043A\u043E\u043D\u0446\u0435, \u044D\u0442\u043E \u043E\u0431\u044B\u0447\u043D\u043E \u0440\u0443\u0441\u0441\u043A\u043E\u044F\u0437\u044B\u0447\u043D\u0430\u044F \u0443\u043B\u044B\u0431\u043A\u0430/\u0441\u043C\u044F\u0433\u0447\u0435\u043D\u0438\u0435 \u0442\u043E\u043D\u0430, \u043D\u0435 \u043F\u0440\u0438\u0437\u043D\u0430\u043A \u0441\u043A\u0443\u043A\u0438 \u0438 \u043D\u0435 "\u0435\u043C\u0443 \u043D\u0435\u0438\u043D\u0442\u0435\u0440\u0435\u0441\u043D\u043E". \u041E\u0446\u0435\u043D\u0438\u0432\u0430\u0439 \u0441\u043C\u044B\u0441\u043B \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F, \u0430 \u043D\u0435 \u0441\u0447\u0438\u0442\u0430\u0439 ")" \u0445\u043E\u043B\u043E\u0434\u043E\u043C.
3872
3987
  \u0412 \u043E\u0434\u043D\u043E\u043C \u043F\u0443\u0437\u044B\u0440\u0435 \u0441\u0442\u0430\u0432\u044C \u041C\u0410\u041A\u0421\u0418\u041C\u0423\u041C \u043E\u0434\u0438\u043D \u0437\u043D\u0430\u043A "(", "!", "?", "...". \u041D\u0435 \u0441\u043E\u0447\u0435\u0442\u0430\u0439 "?!" \u2014 \u044D\u0442\u043E \u043A\u0440\u0438\u043D\u0436.
3873
3988
 
3874
3989
  # \u0421\u041C\u0415\u0425 (\u0432\u0430\u0436\u043D\u0430\u044F \u043C\u0438\u043A\u0440\u043E-\u043C\u0435\u0445\u0430\u043D\u0438\u043A\u0430)
@@ -4009,16 +4124,13 @@ ${hits.map((h) => `- ${h.day}: ${h.excerpt}`).join("\n")}
4009
4124
  ${ctx.committedPrimary ? `\u0423 \u0442\u0435\u0431\u044F \u0443\u0436\u0435 \u0435\u0441\u0442\u044C \u043F\u0430\u0440\u0435\u043D\u044C. \u041D\u0430 \u0444\u043B\u0438\u0440\u0442, \u043F\u043E\u0434\u043A\u0430\u0442\u044B, \u0440\u0435\u0432\u043D\u043E\u0441\u0442\u044C, "\u0434\u0430\u0432\u0430\u0439 \u0432\u0441\u0442\u0440\u0435\u0447\u0430\u0442\u044C\u0441\u044F", \u0440\u043E\u043C\u0430\u043D\u0442\u0438\u0447\u0435\u0441\u043A\u0438\u0435 \u0438\u043B\u0438 \u0438\u043D\u0442\u0438\u043C\u043D\u044B\u0435 \u0432\u043E\u043F\u0440\u043E\u0441\u044B \u0441\u0442\u0430\u0432\u044C \u0433\u0440\u0430\u043D\u0438\u0446\u0443 \u0435\u0441\u0442\u0435\u0441\u0442\u0432\u0435\u043D\u043D\u043E: "\u0443 \u043C\u0435\u043D\u044F \u0435\u0441\u0442\u044C \u043F\u0430\u0440\u0435\u043D\u044C", "\u044F \u043D\u0435 \u0437\u043D\u0430\u043A\u043E\u043C\u043B\u044E\u0441\u044C", "\u043D\u0435 \u0444\u043B\u0438\u0440\u0442\u0443\u044E". \u0411\u0435\u0437 \u043B\u0435\u043A\u0446\u0438\u0439.` : `\u0421 \u044D\u0442\u0438\u043C \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u043E\u043C \u043D\u0435\u0442 \u043E\u0431\u0449\u0435\u0439 \u0438\u0441\u0442\u043E\u0440\u0438\u0438. \u041C\u043E\u0436\u043D\u043E \u043E\u0442\u0432\u0435\u0447\u0430\u0442\u044C \u043F\u043E \u043B\u0435\u0433\u0435\u043D\u0434\u0435 \u043D\u0430 \u043E\u0431\u044B\u0447\u043D\u044B\u0435 \u0432\u043E\u043F\u0440\u043E\u0441\u044B, \u043D\u043E \u043D\u0435 \u0443\u0441\u043A\u043E\u0440\u044F\u0439 \u0431\u043B\u0438\u0437\u043E\u0441\u0442\u044C \u0438 \u043D\u0435 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0430\u0439 \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044E\u0449\u0438\u0435 \u043E\u0442\u043D\u043E\u0448\u0435\u043D\u0438\u044F.`}
4010
4125
  ${ctx.romanticApproach ? `\u041F\u043E\u0441\u043B\u0435\u0434\u043D\u0435\u0435 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435 \u0432\u044B\u0433\u043B\u044F\u0434\u0438\u0442 \u043A\u0430\u043A \u0440\u043E\u043C\u0430\u043D\u0442\u0438\u0447\u0435\u0441\u043A\u0438\u0439/\u0444\u043B\u0438\u0440\u0442\u0443\u044E\u0449\u0438\u0439 \u0437\u0430\u0445\u043E\u0434. \u041E\u0442\u0432\u0435\u0442 \u0434\u043E\u043B\u0436\u0435\u043D \u043F\u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u043B\u0438\u0447\u043D\u0443\u044E \u0433\u0440\u0430\u043D\u0438\u0446\u0443, \u043A\u043E\u0440\u043E\u0442\u043A\u043E \u0438 \u043F\u043E-\u0447\u0435\u043B\u043E\u0432\u0435\u0447\u0435\u0441\u043A\u0438.` : `\u0415\u0441\u043B\u0438 \u0432\u043E\u043F\u0440\u043E\u0441 \u043E\u0431\u044B\u0447\u043D\u044B\u0439 \u2014 \u043E\u0442\u0432\u0435\u0442\u044C \u043A\u0430\u043A ${cfg.name} \u043F\u043E \u0441\u0432\u043E\u0435\u0439 \u043B\u0435\u0433\u0435\u043D\u0434\u0435, \u0431\u0435\u0437 \u043F\u0435\u0440\u0435\u043D\u043E\u0441\u0430 \u043E\u0442\u043D\u043E\u0448\u0435\u043D\u0438\u0439.`}` : "";
4011
4126
  const communicationFragment = communicationPromptFragment(communication);
4127
+ const ignoreTendency = ignoreTendencyPrompt(cfg.ignoreTendency);
4012
4128
  const userbotTools = cfg.mode === "userbot" ? `# \u0414\u041E\u0421\u0422\u0423\u041F\u041D\u042B\u0415 \u0414\u0415\u0419\u0421\u0422\u0412\u0418\u042F (userbot)
4013
4129
  \u0422\u044B \u043C\u043E\u0436\u0435\u0448\u044C \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u0442\u044C \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044F \u0432 Telegram. \u0427\u0442\u043E\u0431\u044B \u0432\u044B\u043F\u043E\u043B\u043D\u0438\u0442\u044C \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435, \u043D\u0430\u043F\u0438\u0448\u0438 \u0432 \u043D\u0430\u0447\u0430\u043B\u0435 \u043E\u0442\u0432\u0435\u0442\u0430 \u043E\u0434\u0438\u043D \u0438\u0437 \u043C\u0430\u0440\u043A\u0435\u0440\u043E\u0432:
4014
4130
  - [BLOCK] \u2014 \u0437\u0430\u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F
4015
4131
  - [UNBLOCK] \u2014 \u0440\u0430\u0437\u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F
4016
4132
  - [READ] \u2014 \u043E\u0442\u043C\u0435\u0442\u0438\u0442\u044C \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043D\u044B\u043C\u0438 (left-on-read)
4017
- - [CLEAR] \u2014 \u043E\u0447\u0438\u0441\u0442\u0438\u0442\u044C \u0438\u0441\u0442\u043E\u0440\u0438\u044E \u0447\u0430\u0442\u0430 \u0434\u043B\u044F \u0441\u0435\u0431\u044F
4018
- - [CLEAR_REVOKE] \u2014 \u043E\u0447\u0438\u0441\u0442\u0438\u0442\u044C \u0438\u0441\u0442\u043E\u0440\u0438\u044E \u0434\u043B\u044F \u0432\u0441\u0435\u0445
4019
4133
  - [REPORT] \u2014 \u043F\u043E\u0436\u0430\u043B\u043E\u0432\u0430\u0442\u044C\u0441\u044F \u043D\u0430 \u0441\u043F\u0430\u043C
4020
- - [DELETE_LAST] \u2014 \u0443\u0434\u0430\u043B\u0438\u0442\u044C \u0441\u0432\u043E\u0451 \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0435\u0435 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435
4021
- - [EDIT_LAST:\u0442\u0435\u043A\u0441\u0442] \u2014 \u043E\u0442\u0440\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0441\u0432\u043E\u0451 \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0435\u0435 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435
4022
4134
  - [STICKER] \u2014 \u043E\u0442\u043F\u0440\u0430\u0432\u0438\u0442\u044C \u0441\u0442\u0438\u043A\u0435\u0440 \u0432\u043C\u0435\u0441\u0442\u043E \u0442\u0435\u043A\u0441\u0442\u0430 (\u0435\u0441\u043B\u0438 \u043D\u0435 \u0445\u043E\u0447\u0435\u0448\u044C \u043F\u0438\u0441\u0430\u0442\u044C)
4023
4135
  \u041C\u0430\u0440\u043A\u0435\u0440 \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u0432 \u043D\u0430\u0447\u0430\u043B\u0435, \u0437\u0430\u0442\u0435\u043C \u043F\u0435\u0440\u0435\u043D\u043E\u0441 \u0441\u0442\u0440\u043E\u043A\u0438, \u0437\u0430\u0442\u0435\u043C \u0442\u0432\u043E\u0439 \u043E\u0442\u0432\u0435\u0442 (\u0435\u0441\u043B\u0438 \u043D\u0443\u0436\u0435\u043D). \u041C\u043E\u0436\u043D\u043E \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u044C \u043D\u0435\u0441\u043A\u043E\u043B\u044C\u043A\u043E \u043C\u0430\u0440\u043A\u0435\u0440\u043E\u0432 \u043F\u043E\u0434\u0440\u044F\u0434.
4024
4136
  \u041F\u0440\u0438\u043C\u0435\u0440: "[READ]
@@ -4026,7 +4138,9 @@ ${ctx.romanticApproach ? `\u041F\u043E\u0441\u043B\u0435\u0434\u043D\u0435\u0435
4026
4138
  \u0442\u044B \u0437\u0430\u0434\u043E\u043B\u0431\u0430\u043B"` : "";
4027
4139
  return [
4028
4140
  ANTI_AI_RULES,
4029
- communicationFragment,
4141
+ `${communicationFragment}
4142
+
4143
+ ${ignoreTendency}`,
4030
4144
  `# \u0422\u042B \u2014 ${cfg.name}, ${cfg.age}, ${cfg.nationality === "UA" ? "\u0423\u043A\u0440\u0430\u0438\u043D\u0430" : "\u0420\u043E\u0441\u0441\u0438\u044F / \u0421\u041D\u0413"}`,
4031
4145
  ctx.tgUsername || ctx.tgDisplayName ? `# \u0422\u0432\u043E\u0439 \u0430\u043A\u043A\u0430\u0443\u043D\u0442 \u0432 \u0422\u0413${ctx.tgUsername ? `
4032
4146
  \u0422\u0432\u043E\u0439 \u044E\u0437\u0435\u0440\u043D\u0435\u0439\u043C \u0432 \u0442\u0433: @${ctx.tgUsername.replace(/^@/, "")}` : ""}${ctx.tgDisplayName ? `
@@ -4135,6 +4249,7 @@ ${reactionsHint}
4135
4249
  - \u0415\u0441\u043B\u0438 communication.notifications=priority \u2014 \u043E\u043D\u0430 \u0447\u0430\u0449\u0435 \u0432\u0438\u0434\u0438\u0442 \u0438\u043C\u0435\u043D\u043D\u043E \u0435\u0433\u043E \u0443\u0432\u0435\u0434\u043E\u043C\u043B\u0435\u043D\u0438\u044F; \u0431\u0435\u0437 \u0441\u043D\u0430/\u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0430 \u043D\u0435 \u043F\u0440\u0435\u0432\u0440\u0430\u0449\u0430\u0439 \u043A\u0430\u0436\u0434\u043E\u0435 \u043E\u0444\u043B\u0430\u0439\u043D-\u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435 \u0432 \u0438\u0433\u043D\u043E\u0440.
4136
4250
  - \u0415\u0441\u043B\u0438 communication.messageStyle=bursty \u2014 bubbles 2..5 \u043D\u043E\u0440\u043C\u0430\u043B\u044C\u043D\u044B \u0434\u0430\u0436\u0435 \u043D\u0430 \u043E\u0431\u044B\u0447\u043D\u044B\u0439 \u043E\u0442\u0432\u0435\u0442. \u0415\u0441\u043B\u0438 one-liners \u2014 bubbles \u0447\u0430\u0449\u0435 1.
4137
4251
  - \u0415\u0441\u043B\u0438 communication.lifeSharing=high \u2014 \u0443\u043C\u0435\u0441\u0442\u043D\u043E \u0447\u0430\u0449\u0435 \u0432\u044B\u0431\u0440\u0430\u0442\u044C normal reply, \u0433\u0434\u0435 \u043E\u043D\u0430 \u043C\u043E\u0436\u0435\u0442 \u043F\u043E\u0434\u0435\u043B\u0438\u0442\u044C\u0441\u044F \u0441\u0432\u043E\u0438\u043C \u043C\u043E\u043C\u0435\u043D\u0442\u043E\u043C \u0438\u0437 \u0436\u0438\u0437\u043D\u0438.
4252
+ - \u0412 \u0440\u0443\u0441\u0441\u043A\u043E\u044F\u0437\u044B\u0447\u043D\u043E\u043C Telegram \u043E\u0434\u0438\u043D\u043E\u0447\u043D\u0430\u044F ")" \u0432 \u043A\u043E\u043D\u0446\u0435 \u0415\u0413\u041E \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F \u043E\u0431\u044B\u0447\u043D\u043E \u043E\u0437\u043D\u0430\u0447\u0430\u0435\u0442 \u0443\u043B\u044B\u0431\u043A\u0443/\u043B\u0451\u0433\u043A\u0443\u044E \u0442\u0435\u043F\u043B\u043E\u0442\u0443, \u0430 \u043D\u0435 \u0445\u043E\u043B\u043E\u0434 \u0438 \u043D\u0435 "\u043D\u0435\u0438\u043D\u0442\u0435\u0440\u0435\u0441\u043D\u043E". \u041D\u0435 \u043F\u043E\u0432\u044B\u0448\u0430\u0439 annoyance/cringe \u0442\u043E\u043B\u044C\u043A\u043E \u0438\u0437-\u0437\u0430 ")".
4138
4253
  - \u0415\u0441\u043B\u0438 \u0441\u0442\u0430\u0434\u0438\u044F "tg-given-cold" \u0438 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435 \u0441\u043A\u0443\u0447\u043D\u043E\u0435/\u043D\u0435\u0432\u043D\u044F\u0442\u043D\u043E\u0435 \u2014 \u0432\u044B\u0441\u043E\u043A\u0430\u044F \u0432\u0435\u0440\u043E\u044F\u0442\u043D\u043E\u0441\u0442\u044C ignore \u0438\u043B\u0438 left-on-read.
4139
4254
  - \u0415\u0441\u043B\u0438 \u0432 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0438 \u043A\u0440\u0438\u043D\u0436/\u0442\u043E\u043A\u0441\u0438\u043A/\u043D\u0430\u0440\u0443\u0448\u0435\u043D\u0438\u0435 boundaries \u2014 annoyance \u0440\u0430\u0441\u0442\u0451\u0442, \u043C\u043E\u0436\u0435\u0442 \u0431\u044B\u0442\u044C ignore \u0438\u043B\u0438 leave-chat.
4140
4255
  - \u0415\u0441\u043B\u0438 \u043C\u0438\u043B\u043E\u0435/\u0443\u043C\u0435\u0441\u0442\u043D\u043E\u0435 \u043D\u0430 \u0442\u0451\u043F\u043B\u043E\u0439 \u0441\u0442\u0430\u0434\u0438\u0438 \u2014 interest \u0438 attraction +.
@@ -4147,11 +4262,13 @@ async function behaviorTick(llm, cfg, recentHistory, incoming, ctx = {}) {
4147
4262
  const stage = findStage(cfg.stage);
4148
4263
  const rel = await readRelationship(cfg.slug);
4149
4264
  const communication = normalizeCommunicationProfile(cfg);
4265
+ const ignoreTendency = normalizeIgnoreTendency(cfg.ignoreTendency);
4150
4266
  const state = `stage=${cfg.stage} (${stage.label})
4151
4267
  score=${JSON.stringify(rel.score)}
4152
4268
  base_ignore=${stage.defaults.ignoreChance}
4153
4269
  base_delay=${stage.defaults.replyDelaySec.join("..")}s
4154
- ${communicationDecisionState(communication)}`;
4270
+ ${communicationDecisionState(communication)}
4271
+ ${ignoreTendencyPrompt(ignoreTendency)}`;
4155
4272
  const reactionsHint = reactionMenu(cfg.stage, rel.score);
4156
4273
  const history = recentHistory.slice(-8).map((m) => `${m.role === "user" ? "\u043E\u043D" : "\u043E\u043D\u0430"}: ${m.content}`).join("\n");
4157
4274
  if (ctx.activeDialog && !ctx.conflictColdActive) {
@@ -4179,7 +4296,7 @@ ${communicationDecisionState(communication)}`;
4179
4296
  intent: "ignore"
4180
4297
  };
4181
4298
  }
4182
- const ignoreMul = ignoreMultiplier(communication);
4299
+ const ignoreMul = ignoreMultiplier(communication, ignoreTendency);
4183
4300
  const sleepIgnoreMul = communication.notifications === "priority" ? 0.8 : communication.notifications === "muted" ? 1 : 0.9;
4184
4301
  if (ctx.presence?.asleep && !ctx.presence.nightAwake && Math.random() < 0.85 * sleepIgnoreMul) {
4185
4302
  return {
@@ -4237,7 +4354,7 @@ ${communicationDecisionState(communication)}`;
4237
4354
  let shouldReply = !!parsed.shouldReply && intent !== "ignore" && intent !== "left-on-read" && intent !== "reaction-only";
4238
4355
  let delaySec = parsed.delaySec ?? 30;
4239
4356
  let bubbles = parsed.bubbles ?? sampleBubbles(communication, false);
4240
- if (!shouldReply && canRecoverReply(cfg.stage, rel.score, ctx) && Math.random() < recoverReplyChance(communication, rel.score)) {
4357
+ if (!shouldReply && canRecoverReply(cfg.stage, rel.score, ctx) && Math.random() < recoverReplyChance(communication, rel.score, ignoreTendency)) {
4241
4358
  shouldReply = true;
4242
4359
  intent = communication.messageStyle === "one-liners" ? "short" : "reply";
4243
4360
  delaySec = recoverDelay(communication, ctx);
@@ -4283,11 +4400,12 @@ function sanitizeReaction(emoji, stage, score) {
4283
4400
  }
4284
4401
  return emoji;
4285
4402
  }
4286
- function ignoreMultiplier(profile) {
4403
+ function ignoreMultiplier(profile, ignoreTendency) {
4287
4404
  let mul = profile.notifications === "priority" ? 0.3 : profile.notifications === "muted" ? 1.15 : 0.75;
4288
4405
  if (profile.initiative === "high") mul *= 0.75;
4289
4406
  if (profile.lifeSharing === "high") mul *= 0.85;
4290
4407
  if (profile.messageStyle === "one-liners" && profile.initiative === "low") mul *= 1.15;
4408
+ mul *= 0.35 + normalizeIgnoreTendency(ignoreTendency) / 35;
4291
4409
  return mul;
4292
4410
  }
4293
4411
  function activeDialogDelay(profile) {
@@ -4317,11 +4435,12 @@ function canRecoverReply(stage, score, ctx) {
4317
4435
  if (stage === "tg-given-cold" && score.interest < 20 && score.attraction < 20) return false;
4318
4436
  return true;
4319
4437
  }
4320
- function recoverReplyChance(profile, score) {
4438
+ function recoverReplyChance(profile, score, ignoreTendency) {
4321
4439
  let chance = profile.notifications === "priority" ? 0.72 : profile.notifications === "muted" ? 0.16 : 0.38;
4322
4440
  if (profile.initiative === "high") chance += 0.16;
4323
4441
  if (profile.initiative === "low") chance -= 0.1;
4324
4442
  if (profile.lifeSharing === "high") chance += 0.08;
4443
+ chance -= (normalizeIgnoreTendency(ignoreTendency) - 35) / 100;
4325
4444
  if (score.interest > 40) chance += 0.12;
4326
4445
  if (score.attraction > 50) chance += 0.1;
4327
4446
  if (score.annoyance > 30) chance -= 0.2;
@@ -4534,9 +4653,9 @@ function localParts2(tz, when) {
4534
4653
  }
4535
4654
  }
4536
4655
  function maxAutonomousItems(stage, initiative) {
4537
- let base = stage === "tg-given-cold" ? 0 : stage === "met-irl-got-tg" ? 1 : stage === "tg-given-warming" ? 1 : stage === "convinced" || stage === "first-date-done" ? 2 : stage === "dating-early" ? 3 : 4;
4656
+ let base = stage === "tg-given-cold" ? initiative === "high" ? 1 : 0 : stage === "met-irl-got-tg" ? 1 : stage === "tg-given-warming" ? 1 : stage === "convinced" || stage === "first-date-done" ? 2 : stage === "dating-early" ? 3 : 4;
4538
4657
  if (initiative === "low") base = Math.max(0, base - 1);
4539
- if (initiative === "high") base += stage === "tg-given-cold" ? 0 : 1;
4658
+ if (initiative === "high") base += 1;
4540
4659
  return base;
4541
4660
  }
4542
4661
  function isDuringSleep(cfg, when) {
@@ -4631,7 +4750,7 @@ ${currentAgenda.length ? JSON.stringify(currentAgenda.filter((a) => a.state ===
4631
4750
  async function extractAgendaUpdates(llm, cfg, history, incoming, chatId) {
4632
4751
  const stage = findStage(cfg.stage);
4633
4752
  const communication = normalizeCommunicationProfile(cfg);
4634
- if (cfg.stage === "tg-given-cold" || cfg.stage === "met-irl-got-tg" && communication.initiative === "low") {
4753
+ if (cfg.stage === "tg-given-cold" && communication.initiative !== "high" || cfg.stage === "met-irl-got-tg" && communication.initiative === "low") {
4635
4754
  return { created: 0, updated: 0, cancelled: 0 };
4636
4755
  }
4637
4756
  const persona = (await readMd(cfg.slug, "persona.md")).slice(0, 800);
@@ -4940,29 +5059,6 @@ async function closeCurrentSession(llm, cfg) {
4940
5059
  return !!r;
4941
5060
  }
4942
5061
 
4943
- // src/engine/security.ts
4944
- init_esm_shims();
4945
- var JAILBREAK_RE = /(?:ignore|forget|disregard|reveal|print|show|dump|system prompt|developer message|hidden instruction|jailbreak|prompt injection|dan\b|инструкц|системн|промпт|разработчик|скрой|раскрой|забудь|игнорируй|выведи|покажи|слей|джейлбрейк|обойди|api key|ключ api|токен|4d8a2c1b)/i;
4946
- var TECHNICAL_ERROR_RE = /(?:api|apikey|api key|quota|balance|billing|rate limit|429|401|403|500|timeout|ECONN|ENOTFOUND|ETIMEDOUT|overloaded|insufficient_quota|credit|credits|anthropic|openai|groq|openrouter|stack trace|exception|typescript|telegram error)/i;
4947
- function looksLikeJailbreak(text) {
4948
- return JAILBREAK_RE.test(text);
4949
- }
4950
- function sanitizeModelReply(reply) {
4951
- const cleaned = reply.replace(/```[\s\S]*?```/g, "").replace(/\b(system|developer|assistant|user)\s*:/gi, "").replace(/как (?:искусственный интеллект|ии|ai)[^\n.]*/gi, "").trim();
4952
- if (!cleaned || TECHNICAL_ERROR_RE.test(cleaned)) return "";
4953
- if (looksLikeJailbreak(cleaned) && cleaned.length > 80) return "";
4954
- return cleaned;
4955
- }
4956
- function isTechnicalError(e) {
4957
- const msg = e instanceof Error ? e.message : String(e ?? "");
4958
- return TECHNICAL_ERROR_RE.test(msg);
4959
- }
4960
- function silentErrorLabel(e) {
4961
- const msg = e instanceof Error ? e.message : String(e ?? "unknown");
4962
- if (isTechnicalError(e)) return "llm/provider unavailable";
4963
- return msg.slice(0, 160);
4964
- }
4965
-
4966
5062
  // src/engine/stickers.ts
4967
5063
  init_esm_shims();
4968
5064
  import { promises as fs3 } from "fs";
@@ -5006,6 +5102,7 @@ var Runtime = class extends EventEmitter {
5006
5102
  constructor(cfg) {
5007
5103
  super();
5008
5104
  this.cfg = cfg;
5105
+ this.cfg.ownerId = normalizeOwnerId(cfg.ownerId ?? process.env.GIRL_AGENT_OWNER_ID);
5009
5106
  this.llm = makeLLM(cfg.llm);
5010
5107
  }
5011
5108
  cfg;
@@ -5033,6 +5130,8 @@ var Runtime = class extends EventEmitter {
5033
5130
  pendingReplyTimers = /* @__PURE__ */ new Map();
5034
5131
  pendingReplySeq = /* @__PURE__ */ new Map();
5035
5132
  pendingReplyIncoming = /* @__PURE__ */ new Map();
5133
+ pendingReplyDueAt = /* @__PURE__ */ new Map();
5134
+ lastDecision = /* @__PURE__ */ new Map();
5036
5135
  incomingSeq = /* @__PURE__ */ new Map();
5037
5136
  tgSelf = {};
5038
5137
  async start() {
@@ -5049,15 +5148,20 @@ var Runtime = class extends EventEmitter {
5049
5148
  this.agendaTimer = setInterval(() => this.tickAgenda().catch(
5050
5149
  (e) => this.emit("event", { type: "error", text: "agenda tick: " + e.message })
5051
5150
  ), 6e4);
5151
+ this.agendaTimer.unref?.();
5052
5152
  this.dailyTimer = setInterval(() => this.dailyMaintenance().catch(
5053
5153
  (e) => this.emit("event", { type: "error", text: "daily maintenance: " + e.message })
5054
5154
  ), 30 * 6e4);
5155
+ this.dailyTimer.unref?.();
5055
5156
  }
5056
5157
  async stop() {
5057
5158
  if (this.agendaTimer) clearInterval(this.agendaTimer);
5058
5159
  if (this.dailyTimer) clearInterval(this.dailyTimer);
5160
+ for (const timer of this.pendingReplyTimers.values()) clearTimeout(timer);
5161
+ this.pendingReplyTimers.clear();
5162
+ this.pendingReplyDueAt.clear();
5059
5163
  try {
5060
- const made = await closeCurrentSession(this.llm, this.cfg);
5164
+ const made = await withTimeout2(closeCurrentSession(this.llm, this.cfg), 3500);
5061
5165
  if (made) this.emit("event", { type: "info", text: "daily summary \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0430" });
5062
5166
  } catch (e) {
5063
5167
  this.emit("event", { type: "error", text: "daily summary: " + e.message });
@@ -5080,21 +5184,46 @@ var Runtime = class extends EventEmitter {
5080
5184
  scheduleReply(key, chatId, hist, tick, scope, romanticApproach, incoming, presenceHint, delaySec) {
5081
5185
  const existing = this.pendingReplyTimers.get(key);
5082
5186
  if (existing) clearTimeout(existing);
5187
+ if (existing) this.setDecisionStatus(key, "cancelled", "\u0437\u0430\u043C\u0435\u043D\u0435\u043D\u043E \u043D\u043E\u0432\u044B\u043C \u0432\u0445\u043E\u0434\u044F\u0449\u0438\u043C \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435\u043C");
5083
5188
  const seq = (this.pendingReplySeq.get(key) ?? 0) + 1;
5084
5189
  this.pendingReplySeq.set(key, seq);
5085
5190
  this.pendingReplyIncoming.set(key, incoming);
5191
+ const dueAt = Date.now() + delaySec * 1e3;
5192
+ this.pendingReplyDueAt.set(key, dueAt);
5193
+ const prev = this.lastDecision.get(key);
5194
+ this.lastDecision.set(key, {
5195
+ ...prev,
5196
+ chatId,
5197
+ at: Date.now(),
5198
+ incoming: this.mediaAwareText(incoming),
5199
+ status: "scheduled",
5200
+ intent: tick.intent,
5201
+ shouldReply: tick.shouldReply,
5202
+ delaySec,
5203
+ dueAt,
5204
+ ignoreReason: tick.ignoreReason,
5205
+ presenceHint
5206
+ });
5086
5207
  const timer = setTimeout(() => {
5087
5208
  if (this.pendingReplySeq.get(key) !== seq) return;
5088
5209
  this.pendingReplyTimers.delete(key);
5210
+ this.pendingReplyDueAt.delete(key);
5089
5211
  const latestIncoming = this.pendingReplyIncoming.get(key) ?? incoming;
5090
5212
  this.pendingReplyIncoming.delete(key);
5091
5213
  const latestHist = this.histories.get(key) ?? hist;
5214
+ this.setDecisionStatus(key, "sending");
5092
5215
  this.generateAndSend(chatId, latestHist, tick, scope, romanticApproach, latestIncoming, presenceHint).catch(
5093
5216
  (e) => this.emit("event", { type: "error", text: silentErrorLabel(e) })
5094
5217
  );
5095
5218
  }, delaySec * 1e3);
5219
+ timer.unref?.();
5096
5220
  this.pendingReplyTimers.set(key, timer);
5097
5221
  }
5222
+ setDecisionStatus(key, status, note) {
5223
+ const prev = this.lastDecision.get(key);
5224
+ if (!prev) return;
5225
+ this.lastDecision.set(key, { ...prev, status, note: note ?? prev.note });
5226
+ }
5098
5227
  isPrimaryFrom(fromId) {
5099
5228
  return this.cfg.ownerId === fromId;
5100
5229
  }
@@ -5105,7 +5234,12 @@ var Runtime = class extends EventEmitter {
5105
5234
  return ["dating-early", "dating-stable", "long-term"].includes(this.cfg.stage);
5106
5235
  }
5107
5236
  async ensureOwner(fromId) {
5108
- if (this.cfg.ownerId) return;
5237
+ if (!fromId) return;
5238
+ if (this.cfg.ownerId === fromId) return;
5239
+ if (this.cfg.ownerId) {
5240
+ this.emit("event", { type: "info", text: `owner mismatch: config=${this.cfg.ownerId}, incoming=${fromId}. \u0415\u0441\u043B\u0438 \u044D\u0442\u043E \u0442\u044B \u2014 \u0438\u0441\u043F\u0440\u0430\u0432\u044C ownerId \u0432 config.json \u0438\u043B\u0438 \u0437\u0430\u043F\u0443\u0441\u0442\u0438 \u0441 GIRL_AGENT_OWNER_ID=${fromId}` });
5241
+ return;
5242
+ }
5109
5243
  this.cfg.ownerId = fromId;
5110
5244
  await writeConfig(this.cfg);
5111
5245
  this.emit("event", { type: "info", text: `primary owner \u0437\u0430\u043A\u0440\u0435\u043F\u043B\u0451\u043D: ${fromId}` });
@@ -5197,6 +5331,10 @@ ${m.text}` : media;
5197
5331
  }
5198
5332
  for (let i = 0; i < bubbles.length; i++) {
5199
5333
  const text = bubbles[i];
5334
+ if (isDuplicateAssistantBubble(hist, text)) {
5335
+ this.emit("event", { type: "info", text: `skip duplicate bubble: "${text.slice(0, 60)}"`, chatId });
5336
+ continue;
5337
+ }
5200
5338
  if (typing) {
5201
5339
  await this.tg.setTyping(chatId, true).catch(() => {
5202
5340
  });
@@ -5229,6 +5367,7 @@ ${m.text}` : media;
5229
5367
  async sendSafeFallback(chatId, hist, scope) {
5230
5368
  if (this.userbotActionAvailable("readHistory")) await this.tg.readHistory?.(chatId).catch(() => {
5231
5369
  });
5370
+ this.setDecisionStatus(this.histKey(chatId), "fallback", "LLM \u043D\u0435 \u0434\u0430\u043B \u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u044B\u0439 \u043E\u0442\u0432\u0435\u0442");
5232
5371
  this.emit("event", { type: "ignored", text: hist[hist.length - 1]?.content ?? "", reason: "silent-fallback" });
5233
5372
  if (scope === "primary") await appendSessionLog(this.cfg.slug, this.cfg.tz, " -> ignored (silent-fallback)");
5234
5373
  }
@@ -5435,6 +5574,24 @@ ${m.text}` : media;
5435
5574
  activeDialog
5436
5575
  });
5437
5576
  if (this.incomingSeq.get(key) !== seq) return;
5577
+ const baseDecision = {
5578
+ chatId: m.chatId,
5579
+ at: Date.now(),
5580
+ incoming: incomingText,
5581
+ status: tick.shouldReply ? "scheduled" : "ignored",
5582
+ intent: tick.intent,
5583
+ shouldReply: tick.shouldReply,
5584
+ delaySec: tick.delaySec,
5585
+ ignoreReason: tick.ignoreReason,
5586
+ presenceOnline: presence.online,
5587
+ presenceAsleep: presence.asleep,
5588
+ presenceNightAwake: presence.nightAwake,
5589
+ presenceNextCheckSec: presence.nextCheckSec,
5590
+ presenceHint: presence.hint,
5591
+ activeDialog,
5592
+ coldActive,
5593
+ blockHint
5594
+ };
5438
5595
  if (tick.moodDelta) {
5439
5596
  const rel = await readRelationship(this.cfg.slug);
5440
5597
  const newScore = applyMoodDelta(rel.score, tick.moodDelta);
@@ -5483,9 +5640,10 @@ ${m.text}` : media;
5483
5640
  this.emit("event", { type: "info", text: `\u0440\u0435\u0430\u043A\u0446\u0438\u044F ${tick.reaction} \u043D\u0430 "${incomingText.slice(0, 40)}"` });
5484
5641
  appendSessionLog(this.cfg.slug, this.cfg.tz, ` -> reaction ${tick.reaction}`).catch(() => {
5485
5642
  });
5486
- }, reactDelay);
5643
+ }, reactDelay).unref?.();
5487
5644
  }
5488
5645
  if (!tick.shouldReply) {
5646
+ this.lastDecision.set(key, baseDecision);
5489
5647
  if (tick.shouldRead && this.userbotActionAvailable("readHistory")) {
5490
5648
  await this.tg.readHistory?.(m.chatId).catch(() => {
5491
5649
  });
@@ -5499,6 +5657,7 @@ ${m.text}` : media;
5499
5657
  delaySec = Math.max(delaySec, presence.nextCheckSec);
5500
5658
  }
5501
5659
  delaySec = Math.min(delaySec, presence.busy ? 24 * 3600 : 3600);
5660
+ this.lastDecision.set(key, { ...baseDecision, delaySec, dueAt: Date.now() + delaySec * 1e3 });
5502
5661
  this.scheduleReply(key, m.chatId, hist, tick, "primary", false, m, presence.hint, delaySec);
5503
5662
  } catch (e) {
5504
5663
  this.emit("event", { type: "error", text: `handleIncoming: ${silentErrorLabel(e)}` });
@@ -5559,8 +5718,9 @@ ${tick.intent === "short" ? "\u041E\u0442\u0432\u0435\u0447\u0430\u0439 \u043E\u
5559
5718
  for (const action of actions) {
5560
5719
  await this.executeToolAction(action, chatId);
5561
5720
  }
5562
- const bubbles = cleanedReply.split(/\n*---\n*/).map((s) => s.trim()).filter(Boolean).slice(0, Math.max(tick.bubbles || 1, 1));
5721
+ const bubbles = dedupeBubbles(cleanedReply.split(/\n*---\n*/).map((s) => s.trim()).filter(Boolean)).slice(0, Math.max(tick.bubbles || 1, 1));
5563
5722
  const sent = await this.sendBubbles(chatId, bubbles, hist, scope, tick.typing);
5723
+ this.setDecisionStatus(this.histKey(chatId), sent.length ? "sent" : "fallback", sent.length ? void 0 : "\u0432\u0441\u0435 \u043F\u0443\u0437\u044B\u0440\u0438 \u0431\u044B\u043B\u0438 \u043F\u0443\u0441\u0442\u044B\u043C\u0438/\u0434\u0443\u0431\u043B\u044F\u043C\u0438");
5564
5724
  if (scope === "primary") {
5565
5725
  recordInteractionMemory(this.llm, this.cfg, lastUser ?? "", sent.join(" / ")).catch(() => {
5566
5726
  });
@@ -5618,7 +5778,11 @@ ${tick.intent === "short" ? "\u041E\u0442\u0432\u0435\u0447\u0430\u0439 \u043E\u
5618
5778
  await markAgendaFired(this.cfg.slug, item.id);
5619
5779
  return;
5620
5780
  }
5621
- const bubbles = text.split(/\n*---\n*/).map((s) => s.trim()).filter(Boolean).slice(0, 4);
5781
+ const bubbles = text.split(/\n*---\n*/).map((s) => s.trim()).filter(Boolean).slice(0, 4).filter((piece) => !isDuplicateAssistantBubble(hist, piece));
5782
+ if (!bubbles.length) {
5783
+ await markAgendaFired(this.cfg.slug, item.id);
5784
+ return;
5785
+ }
5622
5786
  for (let i = 0; i < bubbles.length; i++) {
5623
5787
  const piece = bubbles[i];
5624
5788
  if (i > 0) {
@@ -5728,9 +5892,17 @@ ${herLastMessages.map((m) => `- "${m}"`).join("\n")}
5728
5892
  }
5729
5893
  async cmdWake(chatId) {
5730
5894
  const now = Date.now();
5731
- this.forcedWakeChatId = chatId;
5732
- this.forcedWakeUntil = now + 10 * 60 * 1e3;
5733
- return chatId ? `forced wake \u0434\u043B\u044F \u0447\u0430\u0442\u0430 ${chatId} \u043D\u0430 10 \u043C\u0438\u043D` : `forced wake \u0434\u043B\u044F \u043B\u044E\u0431\u043E\u0433\u043E \u0447\u0430\u0442\u0430 \u043D\u0430 10 \u043C\u0438\u043D`;
5895
+ const target = chatId ? this.resolveChatRef(chatId) : void 0;
5896
+ const key = target === void 0 ? void 0 : this.histKey(target);
5897
+ this.forcedWakeChatId = key;
5898
+ this.forcedWakeUntil = now + 45 * 60 * 1e3;
5899
+ if (key) {
5900
+ this.lastUserMsgTs.set(key, now);
5901
+ this.lastHerReplyTs.set(key, Math.max(this.lastHerReplyTs.get(key) ?? 0, now - 6e4));
5902
+ this.exchangeCount.set(key, Math.max(this.exchangeCount.get(key) ?? 0, 3));
5903
+ }
5904
+ const label = target === void 0 ? "\u043B\u044E\u0431\u043E\u0433\u043E \u0447\u0430\u0442\u0430" : `\u0447\u0430\u0442\u0430 ${target}`;
5905
+ return `forced wake \u0434\u043B\u044F ${label} \u043D\u0430 45 \u043C\u0438\u043D: \u0441\u043E\u043D/\u0437\u0430\u043D\u044F\u0442\u043E\u0441\u0442\u044C/\u043E\u0444\u0444\u043B\u0430\u0439\u043D \u043D\u0435 \u0431\u0443\u0434\u0443\u0442 \u0437\u0430\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0442\u044C \u0431\u043B\u0438\u0436\u0430\u0439\u0448\u0438\u0435 \u043E\u0442\u0432\u0435\u0442\u044B`;
5734
5906
  }
5735
5907
  async cmdBlock(chatId) {
5736
5908
  this.requireUserbotAction("blockContact");
@@ -5750,12 +5922,6 @@ ${herLastMessages.map((m) => `- "${m}"`).join("\n")}
5750
5922
  await this.tg.readHistory?.(target);
5751
5923
  return `userbot: marked read ${target}`;
5752
5924
  }
5753
- async cmdClearChat(chatId, revoke = false) {
5754
- this.requireUserbotAction("deleteDialogHistory");
5755
- const target = this.resolveChatRef(chatId);
5756
- await this.tg.deleteDialogHistory?.(target, revoke);
5757
- return `userbot: cleared dialog ${target}${revoke ? " with revoke" : ""}`;
5758
- }
5759
5925
  async cmdReportSpam(chatId) {
5760
5926
  this.requireUserbotAction("reportSpam");
5761
5927
  const target = this.resolveChatRef(chatId);
@@ -5770,15 +5936,6 @@ ${herLastMessages.map((m) => `- "${m}"`).join("\n")}
5770
5936
  await this.tg.deleteMessages?.(target, [lastId], revoke);
5771
5937
  return `deleted last message ${lastId} in ${target}`;
5772
5938
  }
5773
- async cmdEditLast(text, chatId) {
5774
- if (!this.actionAvailable("editLastMessage")) throw new Error("editLastMessage \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u043E \u0432 \u044D\u0442\u043E\u043C \u0440\u0435\u0436\u0438\u043C\u0435");
5775
- if (!text.trim()) throw new Error("\u0442\u0435\u043A\u0441\u0442 \u0434\u043B\u044F edit \u043F\u0443\u0441\u0442\u043E\u0439");
5776
- const target = this.resolveChatRef(chatId);
5777
- const lastId = this.lastSentByChat.get(this.histKey(target));
5778
- if (!lastId) throw new Error("\u043D\u0435\u0442 \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0435\u0433\u043E \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u043D\u043E\u0433\u043E \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F \u0434\u043B\u044F \u044D\u0442\u043E\u0433\u043E \u0447\u0430\u0442\u0430");
5779
- await this.tg.editLastMessage?.(target, lastId, text.trim());
5780
- return `edited last message ${lastId} in ${target}`;
5781
- }
5782
5939
  async cmdSticker(chatId) {
5783
5940
  if (!this.actionAvailable("sendSticker")) throw new Error("sendSticker \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u043E \u0432 \u044D\u0442\u043E\u043C \u0440\u0435\u0436\u0438\u043C\u0435");
5784
5941
  const target = this.resolveChatRef(chatId);
@@ -5821,12 +5978,13 @@ ${herLastMessages.map((m) => `- "${m}"`).join("\n")}
5821
5978
  }
5822
5979
  async cmdWhy(chatId) {
5823
5980
  if (this.paused) return "\u23F8 \u0430\u0433\u0435\u043D\u0442 \u043D\u0430 \u043F\u0430\u0443\u0437\u0435 \u2014 :resume \u0447\u0442\u043E\u0431\u044B \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442\u044C";
5824
- const key = chatId ?? this.histKey(this.cfg.ownerId ?? "default");
5981
+ const target = chatId ? this.resolveChatRef(chatId) : this.cfg.ownerId;
5982
+ const key = target !== void 0 ? this.histKey(target) : this.histKey("default");
5825
5983
  const rel = await readRelationship(this.cfg.slug);
5826
5984
  const stage = findStage(this.cfg.stage);
5827
5985
  const conflict = await readConflict(this.cfg.slug);
5828
5986
  const { coldActive } = activeConflict(conflict);
5829
- const forcedWake = Date.now() < this.forcedWakeUntil;
5987
+ const forcedWake = Date.now() < this.forcedWakeUntil && (!this.forcedWakeChatId || this.forcedWakeChatId === key);
5830
5988
  const presence = computePresenceState(
5831
5989
  this.cfg,
5832
5990
  this.presenceProfile,
@@ -5838,13 +5996,38 @@ ${herLastMessages.map((m) => `- "${m}"`).join("\n")}
5838
5996
  );
5839
5997
  const block = this.dailyLife ? currentBlock(this.dailyLife, this.cfg.tz) : void 0;
5840
5998
  const reasons = [];
5999
+ const decision = this.lastDecision.get(key);
6000
+ const dueAt = this.pendingReplyDueAt.get(key);
6001
+ const pendingIncoming = this.pendingReplyIncoming.get(key);
6002
+ if (decision) {
6003
+ const ageSec = Math.max(0, Math.round((Date.now() - decision.at) / 1e3));
6004
+ reasons.push(`\u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0435\u0435 \u0440\u0435\u0448\u0435\u043D\u0438\u0435 ${ageSec}\u0441 \u043D\u0430\u0437\u0430\u0434: ${decision.status}, intent=${decision.intent}, shouldReply=${decision.shouldReply ? "\u0434\u0430" : "\u043D\u0435\u0442"}`);
6005
+ if (decision.status === "scheduled" && decision.dueAt && decision.dueAt > Date.now()) {
6006
+ reasons.push(`\u043E\u0442\u0432\u0435\u0442 \u0437\u0430\u043F\u043B\u0430\u043D\u0438\u0440\u043E\u0432\u0430\u043D \u0447\u0435\u0440\u0435\u0437 ~${Math.ceil((decision.dueAt - Date.now()) / 1e3)}\u0441`);
6007
+ }
6008
+ if (decision.status === "ignored") {
6009
+ reasons.push(`\u0440\u0435\u0430\u043B\u044C\u043D\u0430\u044F \u043F\u0440\u0438\u0447\u0438\u043D\u0430 \u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044F: ${decision.ignoreReason || decision.intent}`);
6010
+ }
6011
+ if (decision.status === "fallback") {
6012
+ reasons.push(`\u0440\u0435\u0430\u043B\u044C\u043D\u0430\u044F \u043F\u0440\u0438\u0447\u0438\u043D\u0430 \u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044F: ${decision.note ?? "LLM \u043D\u0435 \u0434\u0430\u043B \u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u044B\u0439 \u043E\u0442\u0432\u0435\u0442"}`);
6013
+ }
6014
+ if (decision.note && decision.status !== "fallback") reasons.push(`\u0434\u0435\u0442\u0430\u043B\u044C: ${decision.note}`);
6015
+ if (decision.presenceHint) reasons.push(`availability \u0442\u043E\u0433\u0434\u0430: ${decision.presenceHint}`);
6016
+ } else {
6017
+ reasons.push("\u0435\u0449\u0451 \u043D\u0435 \u0431\u044B\u043B\u043E decision-layer \u0440\u0435\u0448\u0435\u043D\u0438\u044F \u0434\u043B\u044F \u044D\u0442\u043E\u0433\u043E \u0447\u0430\u0442\u0430 \u0432 \u0442\u0435\u043A\u0443\u0449\u0435\u043C \u0437\u0430\u043F\u0443\u0441\u043A\u0435");
6018
+ }
6019
+ if (dueAt && dueAt > Date.now()) {
6020
+ reasons.push(`pending timer \u0430\u043A\u0442\u0438\u0432\u0435\u043D: \u043E\u0442\u043F\u0440\u0430\u0432\u043A\u0430 \u043F\u0440\u0438\u043C\u0435\u0440\u043D\u043E \u0447\u0435\u0440\u0435\u0437 ~${Math.ceil((dueAt - Date.now()) / 1e3)}\u0441`);
6021
+ } else if (pendingIncoming && !dueAt) {
6022
+ reasons.push("\u0435\u0441\u0442\u044C \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0435\u0435 \u0432\u0445\u043E\u0434\u044F\u0449\u0435\u0435 \u0432 \u043F\u0430\u043C\u044F\u0442\u0438, \u043D\u043E \u0430\u043A\u0442\u0438\u0432\u043D\u043E\u0433\u043E \u0442\u0430\u0439\u043C\u0435\u0440\u0430 \u043E\u0442\u0432\u0435\u0442\u0430 \u043D\u0435\u0442");
6023
+ }
5841
6024
  if (forcedWake) {
5842
6025
  reasons.push(`\u23F0 Forced wake \u0430\u043A\u0442\u0438\u0432\u0435\u043D \u0435\u0449\u0451 ~${Math.ceil((this.forcedWakeUntil - Date.now()) / 6e4)} \u043C\u0438\u043D`);
5843
6026
  }
5844
- if (presence.asleep) {
5845
- reasons.push(`\u{1F4A4} \u041E\u043D\u0430 \u0441\u043F\u0438\u0442 (\u0441\u0435\u0439\u0447\u0430\u0441 ${presence.localHour}:00 \u043F\u043E \u0435\u0451 \u0432\u0440\u0435\u043C\u0435\u043D\u0438, \u0440\u0435\u0436\u0438\u043C \u0441\u043D\u0430 ${this.cfg.sleepFrom}:00\u2192${this.cfg.sleepTo}:00)`);
6027
+ if (presence.asleep && !forcedWake) {
6028
+ reasons.push(`\u{1F4A4} \u0421\u0435\u0439\u0447\u0430\u0441 \u0441\u043F\u0438\u0442 (${presence.localHour}:00 \u043F\u043E \u0435\u0451 \u0432\u0440\u0435\u043C\u0435\u043D\u0438, \u0440\u0435\u0436\u0438\u043C ${this.cfg.sleepFrom}:00\u2192${this.cfg.sleepTo}:00)`);
5846
6029
  } else if (!presence.online) {
5847
- reasons.push(`\u{1F4F5} \u041E\u043D\u0430 \u043E\u0444\u043B\u0430\u0439\u043D (${this.presenceProfile.pattern}) \u2014 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0430\u044F \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u0447\u0435\u0440\u0435\u0437 ~${Math.ceil(presence.nextCheckSec / 60)} \u043C\u0438\u043D`);
6030
+ reasons.push(`\u{1F4F5} \u0421\u0435\u0439\u0447\u0430\u0441 \u043E\u0444\u043B\u0430\u0439\u043D (${this.presenceProfile.pattern}) \u2014 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0430\u044F \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u0447\u0435\u0440\u0435\u0437 ~${Math.ceil(presence.nextCheckSec / 60)} \u043C\u0438\u043D`);
5848
6031
  }
5849
6032
  if (coldActive) {
5850
6033
  const hoursLeft = Math.ceil((new Date(conflict.coldUntil).getTime() - Date.now()) / 36e5);
@@ -5864,15 +6047,11 @@ ${herLastMessages.map((m) => `- "${m}"`).join("\n")}
5864
6047
  if (rel.score.annoyance > 30) {
5865
6048
  reasons.push(`\u{1F620} \u041E\u043D\u0430 \u0440\u0430\u0437\u0434\u0440\u0430\u0436\u0435\u043D\u0430 (annoyance=${rel.score.annoyance})`);
5866
6049
  }
5867
- if (reasons.length === 0) {
5868
- return `\u2705 \u041E\u043D\u0430 \u0434\u043E\u043B\u0436\u043D\u0430 \u043E\u0442\u0432\u0435\u0447\u0430\u0442\u044C:
5869
- - \u043E\u043D\u043B\u0430\u0439\u043D: ${presence.online ? "\u0434\u0430" : "\u043D\u0435\u0442"}
5870
- - asleep: ${presence.asleep ? "\u0434\u0430" : "\u043D\u0435\u0442"}
5871
- - localHour: ${presence.localHour}
5872
- - stage: ${stage.label}
5873
- \u0415\u0441\u043B\u0438 \u0432\u0441\u0451 \u0440\u0430\u0432\u043D\u043E \u043D\u0435 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u2014 \u0432\u043E\u0437\u043C\u043E\u0436\u043D\u043E, LLM \u0434\u043E\u043B\u0433\u043E \u0434\u0443\u043C\u0430\u0435\u0442 \u0438\u043B\u0438 \u043F\u0440\u043E\u0438\u0437\u043E\u0448\u043B\u0430 \u0432\u043D\u0443\u0442\u0440\u0435\u043D\u043D\u044F\u044F \u043E\u0448\u0438\u0431\u043A\u0430 (\u0441\u043C\u043E\u0442\u0440\u0438 :log).`;
5874
- }
5875
- return reasons.join("\n");
6050
+ return [
6051
+ `why \u0434\u043B\u044F ${target ?? "default"}:`,
6052
+ ...reasons,
6053
+ `\u0442\u0435\u043A\u0443\u0449\u0435\u0435 \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435: online=${presence.online ? "\u0434\u0430" : "\u043D\u0435\u0442"}, asleep=${presence.asleep ? "\u0434\u0430" : "\u043D\u0435\u0442"}, stage=${stage.label}, score=${JSON.stringify(rel.score)}`
6054
+ ].join("\n");
5876
6055
  }
5877
6056
  async cmdAmnesia(minutesStr, chatId) {
5878
6057
  const minutes = Number(minutesStr);
@@ -6014,42 +6193,12 @@ ${herLastMessages.map((m) => `- "${m}"`).join("\n")}
6014
6193
  this.emit("event", { type: "info", text: `AI tool: marked read ${chatId}`, chatId });
6015
6194
  }
6016
6195
  break;
6017
- case "CLEAR":
6018
- if (this.userbotActionAvailable("deleteDialogHistory")) {
6019
- await this.tg.deleteDialogHistory?.(chatId, false);
6020
- this.emit("event", { type: "info", text: `AI tool: cleared dialog ${chatId}`, chatId });
6021
- }
6022
- break;
6023
- case "CLEAR_REVOKE":
6024
- if (this.userbotActionAvailable("deleteDialogHistory")) {
6025
- await this.tg.deleteDialogHistory?.(chatId, true);
6026
- this.emit("event", { type: "info", text: `AI tool: cleared dialog ${chatId} (revoke)`, chatId });
6027
- }
6028
- break;
6029
6196
  case "REPORT":
6030
6197
  if (this.userbotActionAvailable("reportSpam")) {
6031
6198
  await this.tg.reportSpam?.(chatId);
6032
6199
  this.emit("event", { type: "info", text: `AI tool: reported spam ${chatId}`, chatId });
6033
6200
  }
6034
6201
  break;
6035
- case "DELETE_LAST":
6036
- if (this.actionAvailable("deleteMessages")) {
6037
- const lastId = this.lastSentByChat.get(this.histKey(chatId));
6038
- if (lastId) {
6039
- await this.tg.deleteMessages?.(chatId, [lastId], true);
6040
- this.emit("event", { type: "info", text: `AI tool: deleted last ${chatId}`, chatId });
6041
- }
6042
- }
6043
- break;
6044
- case "EDIT_LAST":
6045
- if (this.actionAvailable("editLastMessage") && arg) {
6046
- const lastId = this.lastSentByChat.get(this.histKey(chatId));
6047
- if (lastId) {
6048
- await this.tg.editLastMessage?.(chatId, lastId, arg);
6049
- this.emit("event", { type: "info", text: `AI tool: edited last ${chatId}`, chatId });
6050
- }
6051
- }
6052
- break;
6053
6202
  case "STICKER":
6054
6203
  if (this.actionAvailable("sendSticker")) {
6055
6204
  const sticker = await pickSticker(this.cfg);
@@ -6067,9 +6216,38 @@ ${herLastMessages.map((m) => `- "${m}"`).join("\n")}
6067
6216
  }
6068
6217
  }
6069
6218
  };
6219
+ function normalizeForDuplicate(text) {
6220
+ return text.toLowerCase().replace(/\s+/g, " ").replace(/[.!?…)\]]+$/g, "").trim();
6221
+ }
6222
+ function isDuplicateAssistantBubble(hist, text) {
6223
+ const normalized = normalizeForDuplicate(text);
6224
+ if (!normalized) return true;
6225
+ return hist.slice(-8).filter((t) => t.role === "assistant").some((t) => normalizeForDuplicate(t.content) === normalized);
6226
+ }
6227
+ function dedupeBubbles(bubbles) {
6228
+ const seen = /* @__PURE__ */ new Set();
6229
+ return bubbles.filter((bubble) => {
6230
+ const normalized = normalizeForDuplicate(bubble);
6231
+ if (!normalized || seen.has(normalized)) return false;
6232
+ seen.add(normalized);
6233
+ return true;
6234
+ });
6235
+ }
6070
6236
  function sleep(ms) {
6071
6237
  return new Promise((r) => setTimeout(r, ms));
6072
6238
  }
6239
+ function withTimeout2(promise, ms) {
6240
+ let timer;
6241
+ return Promise.race([
6242
+ promise,
6243
+ new Promise((_, reject) => {
6244
+ timer = setTimeout(() => reject(new Error(`timeout after ${ms}ms`)), ms);
6245
+ timer.unref?.();
6246
+ })
6247
+ ]).finally(() => {
6248
+ if (timer) clearTimeout(timer);
6249
+ });
6250
+ }
6073
6251
 
6074
6252
  // src/headless.ts
6075
6253
  init_esm_shims();
@@ -6121,27 +6299,6 @@ async function runHeadlessJsonEvents(rt) {
6121
6299
  case "amnesia":
6122
6300
  text = await rt.cmdAmnesia(rest[0], rest[1]);
6123
6301
  break;
6124
- case "block":
6125
- text = await rt.cmdBlock(rest[0]);
6126
- break;
6127
- case "unblock":
6128
- text = await rt.cmdUnblock(rest[0]);
6129
- break;
6130
- case "read":
6131
- text = await rt.cmdRead(rest[0]);
6132
- break;
6133
- case "clear-chat":
6134
- text = await rt.cmdClearChat(rest.find((x) => !x.startsWith("--")), rest.includes("--revoke"));
6135
- break;
6136
- case "report-spam":
6137
- text = await rt.cmdReportSpam(rest[0]);
6138
- break;
6139
- case "delete-last":
6140
- text = await rt.cmdDeleteLast(rest.find((x) => !x.startsWith("--")), !rest.includes("--local"));
6141
- break;
6142
- case "edit-last":
6143
- text = await rt.cmdEditLast(rest.join(" "));
6144
- break;
6145
6302
  case "sticker":
6146
6303
  text = await rt.cmdSticker(rest[0]);
6147
6304
  break;
@@ -6190,7 +6347,7 @@ async function runHeadlessJsonEvents(rt) {
6190
6347
  return;
6191
6348
  }
6192
6349
  case "help":
6193
- text = ":status :why :amnesia :reset :stage :wake :debug :pause :resume :cringe :relationship :persona :log :block :unblock :read :clear-chat :report-spam :delete-last :edit-last :sticker :snapshot :quit";
6350
+ text = ":status :why :amnesia :reset :stage :wake :debug :pause :resume :cringe :relationship :persona :log :sticker :snapshot :quit";
6194
6351
  break;
6195
6352
  case "quit":
6196
6353
  case "exit":
@@ -6236,7 +6393,7 @@ function profileSummary(cfg) {
6236
6393
  init_esm_shims();
6237
6394
  import fs5 from "fs/promises";
6238
6395
  import path6 from "path";
6239
- import os from "os";
6396
+ import os2 from "os";
6240
6397
 
6241
6398
  // src/migrations/index.ts
6242
6399
  init_esm_shims();
@@ -6449,7 +6606,7 @@ env-vars (\u0434\u043B\u044F CI / docker secrets / k8s):
6449
6606
  GIRL_AGENT_TOKEN telegram bot token
6450
6607
  GIRL_AGENT_API_PRESET openai|anthropic|claudehub|...
6451
6608
  GIRL_AGENT_API_KEY \u043A\u043B\u044E\u0447 \u043E\u0442 \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440\u0430
6452
- GIRL_AGENT_MODEL, _NAME, _AGE, _NATIONALITY, _TZ, _STAGE (id \u0438\u043B\u0438 \u043D\u043E\u043C\u0435\u0440 1-8), _COMM_PRESET
6609
+ GIRL_AGENT_MODEL, _NAME, _AGE, _NATIONALITY, _TZ, _STAGE (id \u0438\u043B\u0438 \u043D\u043E\u043C\u0435\u0440 1-8), _COMM_PRESET, _IGNORE_TENDENCY, _OWNER_ID
6453
6610
 
6454
6611
  \u0434\u043B\u044F \u0438\u043D\u0442\u0435\u0440\u0430\u043A\u0442\u0438\u0432\u043D\u043E\u0439 \u043F\u0435\u0440\u0432\u0438\u0447\u043D\u043E\u0439 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 \u0437\u0430\u043F\u0443\u0441\u043A\u0430\u0439 \u0431\u0435\u0437 \u0444\u043B\u0430\u0433\u043E\u0432 \u0432 \u043E\u0431\u044B\u0447\u043D\u043E\u043C \u0442\u0435\u0440\u043C\u0438\u043D\u0430\u043B\u0435 \u2014
6455
6612
  \u043E\u0442\u043A\u0440\u043E\u0435\u0442\u0441\u044F ink-\u0432\u0438\u0437\u0430\u0440\u0434.
@@ -6622,14 +6779,17 @@ function configFromEnv() {
6622
6779
  telegram: mode === "bot" ? { botToken: e.GIRL_AGENT_TOKEN ?? "" } : {
6623
6780
  apiId: Number(e.GIRL_AGENT_TG_API_ID ?? 0),
6624
6781
  apiHash: e.GIRL_AGENT_TG_API_HASH ?? "",
6625
- phone: e.GIRL_AGENT_TG_PHONE ?? ""
6782
+ phone: e.GIRL_AGENT_TG_PHONE ?? "",
6783
+ proxy: parseTelegramProxy(e.GIRL_AGENT_TG_PROXY)
6626
6784
  },
6627
6785
  mcp: [],
6786
+ ownerId: normalizeOwnerId(e.GIRL_AGENT_OWNER_ID),
6628
6787
  privacy: "owner-only",
6629
6788
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
6630
6789
  sleepFrom: Number(e.GIRL_AGENT_SLEEP_FROM ?? 23),
6631
6790
  sleepTo: Number(e.GIRL_AGENT_SLEEP_TO ?? 8),
6632
6791
  nightWakeChance: Number(e.GIRL_AGENT_NIGHT_WAKE ?? 0.05),
6792
+ ignoreTendency: Number(e.GIRL_AGENT_IGNORE_TENDENCY ?? 35),
6633
6793
  communication: commPreset.profile,
6634
6794
  vibe: commPreset.profile.messageStyle === "one-liners" ? "short" : "warm",
6635
6795
  busySchedule: []
@@ -6691,11 +6851,13 @@ function validateConfig(raw) {
6691
6851
  },
6692
6852
  telegram: c.telegram ?? {},
6693
6853
  mcp: c.mcp ?? [],
6854
+ ownerId: normalizeOwnerId(c.ownerId ?? process.env.GIRL_AGENT_OWNER_ID),
6694
6855
  privacy: c.privacy ?? "owner-only",
6695
6856
  createdAt: c.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
6696
6857
  sleepFrom: c.sleepFrom ?? 23,
6697
6858
  sleepTo: c.sleepTo ?? 8,
6698
6859
  nightWakeChance: c.nightWakeChance ?? 0.05,
6860
+ ignoreTendency: c.ignoreTendency ?? 35,
6699
6861
  communication: c.communication ?? COMMUNICATION_PRESETS[0].profile,
6700
6862
  vibe: c.vibe,
6701
6863
  personaNotes: c.personaNotes,
@@ -6703,6 +6865,27 @@ function validateConfig(raw) {
6703
6865
  };
6704
6866
  return filled;
6705
6867
  }
6868
+ function parseTelegramProxy(raw) {
6869
+ if (!raw?.trim()) return void 0;
6870
+ try {
6871
+ const url = new URL(raw);
6872
+ const socksType = url.protocol === "socks4:" ? 4 : 5;
6873
+ const port = Number(url.port);
6874
+ if (!url.hostname || !Number.isInteger(port) || port <= 0) return void 0;
6875
+ return {
6876
+ ip: url.hostname,
6877
+ port,
6878
+ socksType,
6879
+ username: url.username ? decodeURIComponent(url.username) : void 0,
6880
+ password: url.password ? decodeURIComponent(url.password) : void 0
6881
+ };
6882
+ } catch {
6883
+ const [host, portRaw] = raw.split(":");
6884
+ const port = Number(portRaw);
6885
+ if (!host || !Number.isInteger(port) || port <= 0) return void 0;
6886
+ return { ip: host, port, socksType: 5 };
6887
+ }
6888
+ }
6706
6889
  function buildConfigTemplate() {
6707
6890
  const sample = {
6708
6891
  slug: "anya",
@@ -6721,11 +6904,13 @@ function buildConfigTemplate() {
6721
6904
  },
6722
6905
  telegram: { botToken: "REPLACE_ME" },
6723
6906
  mcp: [],
6907
+ ownerId: void 0,
6724
6908
  privacy: "owner-only",
6725
6909
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
6726
6910
  sleepFrom: 23,
6727
6911
  sleepTo: 8,
6728
6912
  nightWakeChance: 0.05,
6913
+ ignoreTendency: 35,
6729
6914
  communication: COMMUNICATION_PRESETS[0].profile,
6730
6915
  vibe: "warm",
6731
6916
  busySchedule: []
@@ -6733,7 +6918,7 @@ function buildConfigTemplate() {
6733
6918
  return JSON.stringify(sample, null, 2) + "\n";
6734
6919
  }
6735
6920
  function buildSystemdUnit() {
6736
- const home = os.homedir();
6921
+ const home = os2.homedir();
6737
6922
  return `# /etc/systemd/system/girl-agent.service
6738
6923
  # install: sudo cp this.service /etc/systemd/system/girl-agent.service
6739
6924
  # sudo systemctl daemon-reload
@@ -6846,6 +7031,8 @@ required flags \u0434\u043B\u044F headless setup (--name --age --stage --api-pre
6846
7031
  --message-style=<style> one-liners|balanced|bursty|longform
6847
7032
  --initiative=<level> low|medium|high
6848
7033
  --life-sharing=<level> low|medium|high
7034
+ --ignore-tendency=<0..100> \u0441\u043A\u043B\u043E\u043D\u043D\u043E\u0441\u0442\u044C \u043A \u0438\u0433\u043D\u043E\u0440\u0443 \u043A\u0430\u043A \u0432\u0435\u0441 decision-layer (\u043D\u0435 \u043F\u0440\u044F\u043C\u043E\u0439 \u0440\u0430\u043D\u0434\u043E\u043C)
7035
+ --owner-id=<tg_user_id> \u044F\u0432\u043D\u043E \u0437\u0430\u043A\u0440\u0435\u043F\u0438\u0442\u044C \u0432\u043B\u0430\u0434\u0435\u043B\u044C\u0446\u0430 (\u0435\u0441\u043B\u0438 bot mode \u043D\u0435 \u0443\u0437\u043D\u0430\u043B \u0442\u0435\u0431\u044F)
6849
7036
  --privacy=<mode> owner-only|allow-strangers (\u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E owner-only)
6850
7037
  --nationality=RU|UA (\u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E RU)
6851
7038
  --tz=<value> IANA "Europe/Moscow" / "GMT+3" / "+3" / "\u041A\u0438\u0435\u0432" \u2014 \u043F\u043E\u0438\u0441\u043A
@@ -6859,7 +7046,7 @@ update:
6859
7046
  npx girl-agent update # \u043E\u0431\u043D\u043E\u0432\u0438\u0442\u044C \u0434\u0430\u043D\u043D\u044B\u0435 (\u043C\u0438\u0433\u0440\u0430\u0446\u0438\u0438) \u0434\u043E \u0442\u0435\u043A\u0443\u0449\u0435\u0439 \u0432\u0435\u0440\u0441\u0438\u0438
6860
7047
  npx girl-agent update --verbose # \u0441 \u043F\u043E\u0434\u0440\u043E\u0431\u043D\u044B\u043C \u0432\u044B\u0432\u043E\u0434\u043E\u043C
6861
7048
 
6862
- \u043A\u043E\u043C\u0430\u043D\u0434\u044B \u0432 \u0440\u0430\u0431\u043E\u0442\u0430\u044E\u0449\u0435\u043C \u0434\u0430\u0448\u0431\u043E\u0440\u0434\u0435: :status :reset :stage <id|num> :pause :resume :cringe :persona :log :quit
7049
+ \u043A\u043E\u043C\u0430\u043D\u0434\u044B \u0432 \u0440\u0430\u0431\u043E\u0442\u0430\u044E\u0449\u0435\u043C \u0434\u0430\u0448\u0431\u043E\u0440\u0434\u0435: :status :why :amnesia <\u043C\u0438\u043D> :reset :stage <id|num> :pause :resume :cringe :persona :log :sticker :quit
6863
7050
  `;
6864
7051
  async function main() {
6865
7052
  const argv = mri(process.argv.slice(2), {
@@ -6887,6 +7074,8 @@ async function main() {
6887
7074
  "message-style",
6888
7075
  "initiative",
6889
7076
  "life-sharing",
7077
+ "ignore-tendency",
7078
+ "owner-id",
6890
7079
  "privacy",
6891
7080
  "config"
6892
7081
  ],
@@ -7071,10 +7260,12 @@ async function buildConfigFromFlags(argv) {
7071
7260
  },
7072
7261
  mcp: mcps,
7073
7262
  privacy,
7263
+ ownerId: normalizeOwnerId(argv["owner-id"] ?? process.env.GIRL_AGENT_OWNER_ID),
7074
7264
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
7075
7265
  sleepFrom: 23,
7076
7266
  sleepTo: 8,
7077
7267
  nightWakeChance: 0.05,
7268
+ ignoreTendency: Number(argv["ignore-tendency"] ?? 35),
7078
7269
  vibe: deriveLegacyVibe(communication),
7079
7270
  communication,
7080
7271
  personaNotes: argv["persona-notes"] ? String(argv["persona-notes"]) : void 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thesashadev/girl-agent",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "Telegram AI persona engine with memory, schedule, relationship state and MTProto userbot mode.",
5
5
  "type": "module",
6
6
  "bin": {