omnius 1.0.59 → 1.0.61

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -12159,17 +12159,26 @@ process.on('unhandledRejection', (reason) => {
12159
12159
 
12160
12160
  dlog('COHERE response: ' + _cData.queryId + ' model=' + _cModel + ' tier=' + ['trivial','moderate','complex','expert'][_cTier] + ' ' + _cLatency + 'ms' + (_cReviewed ? ' (reviewed)' : ''));
12161
12161
  // IK-01: Update identity on query success
12162
+ // Store full query + response content; downstream renderers (narrative summary, prompts)
12163
+ // are responsible for compaction. Truncating at write time destroys identity history.
12164
+ // NOTE: '\\n' is double-escaped because this whole block runs inside the
12165
+ // outer DAEMON_SCRIPT template literal — see comment near line 440.
12162
12166
  _updateIdentity({ type: 'query_served', outcome: 1.0, details: {
12163
12167
  latencyMs: _cLatency, toolsUsed: _cToolsUsed, model: _cModel,
12164
- summary: (_cData.query || '').slice(0, 60)
12168
+ query: _cData.query || '',
12169
+ response: _cFinalContent || '',
12170
+ summary: (_cData.query || '') + (_cFinalContent ? '\\n→ ' + _cFinalContent : '')
12165
12171
  }});
12166
12172
  } catch (_cErr) {
12167
12173
  dlog('COHERE inference error: ' + (_cErr.message || _cErr));
12168
12174
  _cohereStats.queriesErrors++;
12169
12175
  _saveStats();
12170
12176
  // IK-01: Update identity on query failure
12177
+ // Preserve the full error so post-hoc diagnosis is possible.
12171
12178
  _updateIdentity({ type: 'query_failed', outcome: 0, details: {
12172
- error: (_cErr.message || String(_cErr)).slice(0, 100), summary: 'inference error'
12179
+ error: (_cErr && _cErr.message) || String(_cErr),
12180
+ query: _cData.query || '',
12181
+ summary: 'inference error: ' + ((_cErr && _cErr.message) || String(_cErr))
12173
12182
  }});
12174
12183
  }
12175
12184
  } catch (_cParseErr) {
@@ -561982,8 +561991,7 @@ function wrapTaskCompleteSummary(summary) {
561982
561991
  }
561983
561992
  function wrapPlainLine(line, width) {
561984
561993
  const out = [];
561985
- const indent = line.match(/^\s*/)?.[0] ?? "";
561986
- const continuationIndent = indent.length > 0 ? indent : "";
561994
+ const continuationIndent = hangingIndentForPlainLine(line, width);
561987
561995
  let remaining = line;
561988
561996
  while (remaining.length > width) {
561989
561997
  let breakAt = remaining.lastIndexOf(" ", width);
@@ -561994,6 +562002,20 @@ function wrapPlainLine(line, width) {
561994
562002
  out.push(remaining);
561995
562003
  return out;
561996
562004
  }
562005
+ function hangingIndentForPlainLine(line, width) {
562006
+ const capped = (n2) => " ".repeat(Math.max(0, Math.min(n2, width - 4)));
562007
+ const patterns = [
562008
+ /^(\s*(?:[-*+○•]\s+))/,
562009
+ /^(\s*(?:\d+[.)]\s+))/,
562010
+ /^(\s*(?:>\s*))/,
562011
+ /^(\s*(?:[A-Za-z][\w.-]{0,28}:\s+))/
562012
+ ];
562013
+ for (const pattern of patterns) {
562014
+ const match = line.match(pattern);
562015
+ if (match?.[1]) return capped(match[1].length);
562016
+ }
562017
+ return capped(line.match(/^\s*/)?.[0].length ?? 0);
562018
+ }
561997
562019
  function renderTaskIncomplete(turns, toolCalls, durationMs, tokens) {
561998
562020
  const duration = formatDuration2(durationMs);
561999
562021
  const tokenStr = tokens ? ` ${formatTokenCount(tokens)}` : "";
@@ -563699,7 +563721,7 @@ var init_voice_session = __esm({
563699
563721
 
563700
563722
  // packages/cli/src/tui/scoped-personality.ts
563701
563723
  import { createHash as createHash17 } from "node:crypto";
563702
- import { existsSync as existsSync82, mkdirSync as mkdirSync46, readFileSync as readFileSync65, writeFileSync as writeFileSync41 } from "node:fs";
563724
+ import { appendFileSync as appendFileSync4, existsSync as existsSync82, mkdirSync as mkdirSync46, readFileSync as readFileSync65, writeFileSync as writeFileSync41 } from "node:fs";
563703
563725
  import { join as join98, resolve as resolve37 } from "node:path";
563704
563726
  function safeName(input) {
563705
563727
  return input.replace(/[^A-Za-z0-9_.-]/g, "-").slice(0, 80) || "default";
@@ -563715,7 +563737,8 @@ function scopedPersonalityPaths(scope) {
563715
563737
  const prefix = `${safeName(scope.label)}-${scopeHash(scope)}`;
563716
563738
  return {
563717
563739
  json: join98(dir, `${prefix}.json`),
563718
- markdown: join98(dir, `${prefix}.md`)
563740
+ markdown: join98(dir, `${prefix}.md`),
563741
+ events: join98(dir, `${prefix}.events.jsonl`)
563719
563742
  };
563720
563743
  }
563721
563744
  function inferToneTags(text) {
@@ -563776,16 +563799,49 @@ function newDocument(scope) {
563776
563799
  "Do not expose private local files, secrets, credentials, or hidden reasoning in public or group scopes.",
563777
563800
  "Do not claim memory is unavailable when scoped context is present."
563778
563801
  ],
563802
+ topicCounts: {},
563803
+ durableObservations: [],
563804
+ relationshipEvents: [],
563779
563805
  recentObservations: []
563780
563806
  };
563781
563807
  }
563808
+ function normalizeDocument(scope, parsed) {
563809
+ const fresh = newDocument(scope);
563810
+ const doc = {
563811
+ ...fresh,
563812
+ ...parsed,
563813
+ scope: {
563814
+ kind: scope.kind,
563815
+ idHash: scopeHash(scope),
563816
+ label: scope.label || parsed.scope?.label || fresh.scope.label
563817
+ },
563818
+ participants: parsed.participants && typeof parsed.participants === "object" ? parsed.participants : {},
563819
+ dominantTone: Array.isArray(parsed.dominantTone) ? parsed.dominantTone : [],
563820
+ responseStyle: Array.isArray(parsed.responseStyle) && parsed.responseStyle.length > 0 ? parsed.responseStyle : fresh.responseStyle,
563821
+ relationshipModel: Array.isArray(parsed.relationshipModel) && parsed.relationshipModel.length > 0 ? parsed.relationshipModel : fresh.relationshipModel,
563822
+ behavioralGuidance: Array.isArray(parsed.behavioralGuidance) && parsed.behavioralGuidance.length > 0 ? parsed.behavioralGuidance : fresh.behavioralGuidance,
563823
+ boundaries: Array.isArray(parsed.boundaries) && parsed.boundaries.length > 0 ? parsed.boundaries : fresh.boundaries,
563824
+ topicCounts: parsed.topicCounts && typeof parsed.topicCounts === "object" ? parsed.topicCounts : {},
563825
+ durableObservations: Array.isArray(parsed.durableObservations) ? parsed.durableObservations.slice(-MAX_PROFILE_DURABLE_OBSERVATIONS) : [],
563826
+ relationshipEvents: Array.isArray(parsed.relationshipEvents) ? parsed.relationshipEvents.slice(-MAX_PROFILE_RELATIONSHIP_EVENTS) : [],
563827
+ recentObservations: Array.isArray(parsed.recentObservations) ? parsed.recentObservations.slice(-MAX_PROFILE_OBSERVATIONS) : []
563828
+ };
563829
+ doc.messageCount = Number.isFinite(doc.messageCount) ? doc.messageCount : 0;
563830
+ for (const profile of Object.values(doc.participants)) {
563831
+ if (!Array.isArray(profile.toneTags)) profile.toneTags = [];
563832
+ if (!Array.isArray(profile.samples)) profile.samples = [];
563833
+ profile.toneTags = profile.toneTags.slice(0, MAX_PROFILE_TAGS);
563834
+ profile.samples = profile.samples.slice(-MAX_PROFILE_SAMPLES);
563835
+ }
563836
+ return doc;
563837
+ }
563782
563838
  function loadScopedPersonality(scope) {
563783
563839
  const paths = scopedPersonalityPaths(scope);
563784
563840
  if (!existsSync82(paths.json)) return newDocument(scope);
563785
563841
  try {
563786
563842
  const parsed = JSON.parse(readFileSync65(paths.json, "utf8"));
563787
563843
  if (parsed.version !== PROFILE_VERSION) return newDocument(scope);
563788
- return parsed;
563844
+ return normalizeDocument(scope, parsed);
563789
563845
  } catch {
563790
563846
  return newDocument(scope);
563791
563847
  }
@@ -563796,6 +563852,14 @@ function saveScopedPersonality(scope, doc) {
563796
563852
  writeFileSync41(paths.json, JSON.stringify(doc, null, 2) + "\n", "utf8");
563797
563853
  writeFileSync41(paths.markdown, renderScopedPersonalityMarkdown(doc) + "\n", "utf8");
563798
563854
  }
563855
+ function appendScopedPersonalityEvent(scope, event) {
563856
+ try {
563857
+ const paths = scopedPersonalityPaths(scope);
563858
+ mkdirSync46(scopedPersonalityDir(scope.repoRoot, scope.kind), { recursive: true });
563859
+ appendFileSync4(paths.events, JSON.stringify(event) + "\n", "utf8");
563860
+ } catch {
563861
+ }
563862
+ }
563799
563863
  function updateScopedPersonality(scope, observation) {
563800
563864
  const doc = loadScopedPersonality(scope);
563801
563865
  const now = new Date(observation.ts ?? Date.now()).toISOString();
@@ -563806,6 +563870,7 @@ function updateScopedPersonality(scope, observation) {
563806
563870
  ...inferToneTags(observation.text),
563807
563871
  ...keywords(observation.text)
563808
563872
  ];
563873
+ const topicTags = keywords(observation.text);
563809
563874
  doc.updatedAt = now;
563810
563875
  doc.messageCount += 1;
563811
563876
  doc.scope.label = scope.label;
@@ -563831,6 +563896,9 @@ function updateScopedPersonality(scope, observation) {
563831
563896
  for (const tag of profile.toneTags) allTags.set(tag, (allTags.get(tag) ?? 0) + profile.count);
563832
563897
  }
563833
563898
  doc.dominantTone = [...allTags.entries()].sort((a2, b) => b[1] - a2[1]).map(([tag]) => tag).slice(0, MAX_PROFILE_TAGS);
563899
+ for (const tag of topicTags) {
563900
+ doc.topicCounts[tag] = (doc.topicCounts[tag] ?? 0) + 1;
563901
+ }
563834
563902
  if (doc.dominantTone.includes("frustrated")) {
563835
563903
  doc.responseStyle = [
563836
563904
  "Start with the concrete fix or answer; avoid soothing filler.",
@@ -563852,15 +563920,31 @@ function updateScopedPersonality(scope, observation) {
563852
563920
  for (const hint of relationshipHints) {
563853
563921
  const clean5 = compactLine(hint, 180);
563854
563922
  if (clean5 && !doc.relationshipModel.includes(clean5)) doc.relationshipModel.push(clean5);
563923
+ if (clean5 && !doc.relationshipEvents.includes(`${now} ${clean5}`)) doc.relationshipEvents.push(`${now} ${clean5}`);
563855
563924
  }
563856
563925
  doc.relationshipModel = doc.relationshipModel.slice(-12);
563926
+ doc.relationshipEvents = doc.relationshipEvents.slice(-MAX_PROFILE_RELATIONSHIP_EVENTS);
563857
563927
  if (text) {
563858
563928
  const line = `${now} ${speaker}${observation.mode ? `/${observation.mode}` : ""}: ${text}`;
563859
563929
  doc.recentObservations.push(line);
563860
563930
  if (doc.recentObservations.length > MAX_PROFILE_OBSERVATIONS) {
563861
563931
  doc.recentObservations.splice(0, doc.recentObservations.length - MAX_PROFILE_OBSERVATIONS);
563862
563932
  }
563933
+ doc.durableObservations.push(line);
563934
+ if (doc.durableObservations.length > MAX_PROFILE_DURABLE_OBSERVATIONS) {
563935
+ doc.durableObservations.splice(0, doc.durableObservations.length - MAX_PROFILE_DURABLE_OBSERVATIONS);
563936
+ }
563863
563937
  }
563938
+ appendScopedPersonalityEvent(scope, {
563939
+ ts: observation.ts ?? Date.now(),
563940
+ iso: now,
563941
+ speaker,
563942
+ role: observation.role,
563943
+ mode: observation.mode,
563944
+ text,
563945
+ toneTags: [...new Set(toneTags)].slice(0, 32),
563946
+ relationshipHints
563947
+ });
563864
563948
  saveScopedPersonality(scope, doc);
563865
563949
  return doc;
563866
563950
  }
@@ -563871,6 +563955,9 @@ function renderScopedPersonalityMarkdown(doc) {
563871
563955
  return `- ${name10}: messages=${profile.count}; last=${profile.lastSeenAt};${tags}${samples ? `
563872
563956
  ${samples}` : ""}`;
563873
563957
  });
563958
+ const topTopics = Object.entries(doc.topicCounts ?? {}).sort((a2, b) => b[1] - a2[1]).slice(0, 18).map(([topic, count]) => `- ${topic}: ${count}`);
563959
+ const durable = doc.durableObservations.slice(-18).map((line) => `- ${line}`);
563960
+ const relationships = doc.relationshipEvents.slice(-10).map((line) => `- ${line}`);
563874
563961
  return [
563875
563962
  `# Scoped Personality Profile`,
563876
563963
  ``,
@@ -563887,6 +563974,11 @@ ${samples}` : ""}`;
563887
563974
  `## Relationship Model`,
563888
563975
  doc.relationshipModel.map((line) => `- ${line}`).join("\n"),
563889
563976
  ``,
563977
+ `## Durable Profile Memory`,
563978
+ topTopics.length ? [`Top recurring topics:`, ...topTopics].join("\n") : "Top recurring topics:\n- none",
563979
+ durable.length ? [`Longer-term observations:`, ...durable].join("\n") : "Longer-term observations:\n- none",
563980
+ relationships.length ? [`Relationship events:`, ...relationships].join("\n") : "Relationship events:\n- none",
563981
+ ``,
563890
563982
  `## Behavioral Guidance`,
563891
563983
  doc.behavioralGuidance.map((line) => `- ${line}`).join("\n"),
563892
563984
  ``,
@@ -563910,13 +564002,15 @@ function renderScopedPersonalityContext(doc) {
563910
564002
  function buildScopedPersonalityContext(scope) {
563911
564003
  return renderScopedPersonalityContext(loadScopedPersonality(scope));
563912
564004
  }
563913
- var PROFILE_VERSION, MAX_PROFILE_OBSERVATIONS, MAX_PROFILE_SAMPLES, MAX_PROFILE_TAGS, STOPWORDS2;
564005
+ var PROFILE_VERSION, MAX_PROFILE_OBSERVATIONS, MAX_PROFILE_DURABLE_OBSERVATIONS, MAX_PROFILE_RELATIONSHIP_EVENTS, MAX_PROFILE_SAMPLES, MAX_PROFILE_TAGS, STOPWORDS2;
563914
564006
  var init_scoped_personality = __esm({
563915
564007
  "packages/cli/src/tui/scoped-personality.ts"() {
563916
564008
  "use strict";
563917
564009
  PROFILE_VERSION = 1;
563918
- MAX_PROFILE_OBSERVATIONS = 18;
563919
- MAX_PROFILE_SAMPLES = 8;
564010
+ MAX_PROFILE_OBSERVATIONS = 40;
564011
+ MAX_PROFILE_DURABLE_OBSERVATIONS = 240;
564012
+ MAX_PROFILE_RELATIONSHIP_EVENTS = 120;
564013
+ MAX_PROFILE_SAMPLES = 16;
563920
564014
  MAX_PROFILE_TAGS = 12;
563921
564015
  STOPWORDS2 = /* @__PURE__ */ new Set([
563922
564016
  "about",
@@ -567727,7 +567821,7 @@ __export(omnius_directory_exports, {
567727
567821
  writeIndexMeta: () => writeIndexMeta,
567728
567822
  writeTaskHandoff: () => writeTaskHandoff2
567729
567823
  });
567730
- import { cpSync as cpSync2, existsSync as existsSync87, mkdirSync as mkdirSync50, readFileSync as readFileSync70, writeFileSync as writeFileSync45, readdirSync as readdirSync29, statSync as statSync32, unlinkSync as unlinkSync15, openSync as openSync2, closeSync as closeSync2, renameSync as renameSync4 } from "node:fs";
567824
+ import { appendFileSync as appendFileSync5, cpSync as cpSync2, existsSync as existsSync87, mkdirSync as mkdirSync50, readFileSync as readFileSync70, writeFileSync as writeFileSync45, readdirSync as readdirSync29, statSync as statSync32, unlinkSync as unlinkSync15, openSync as openSync2, closeSync as closeSync2, renameSync as renameSync4 } from "node:fs";
567731
567825
  import { join as join104, relative as relative9, basename as basename17, dirname as dirname29 } from "node:path";
567732
567826
  import { homedir as homedir31 } from "node:os";
567733
567827
  import { createHash as createHash20 } from "node:crypto";
@@ -568248,6 +568342,7 @@ function saveSessionContext(repoRoot, entry) {
568248
568342
  const contextDir = join104(repoRoot, OMNIUS_DIR, "context");
568249
568343
  mkdirSync50(contextDir, { recursive: true });
568250
568344
  const filePath = join104(contextDir, CONTEXT_SAVE_FILE);
568345
+ const ledgerPath = join104(contextDir, CONTEXT_LEDGER_FILE);
568251
568346
  const lockPath = join104(contextDir, CONTEXT_SAVE_FILE + ".lock");
568252
568347
  const locked = acquireLock(lockPath);
568253
568348
  if (!locked) {
@@ -568264,7 +568359,23 @@ function saveSessionContext(repoRoot, entry) {
568264
568359
  } catch {
568265
568360
  ctx3 = { entries: [], maxEntries: MAX_CONTEXT_ENTRIES, updatedAt: "" };
568266
568361
  }
568362
+ ctx3.maxEntries = Math.max(
568363
+ Number.isFinite(ctx3.maxEntries) ? ctx3.maxEntries : 0,
568364
+ MAX_CONTEXT_ENTRIES
568365
+ );
568267
568366
  const normalizedEntry = normalizeSessionContextEntry(entry);
568367
+ try {
568368
+ appendFileSync5(
568369
+ ledgerPath,
568370
+ JSON.stringify({
568371
+ ...normalizedEntry,
568372
+ ledgerSavedAt: (/* @__PURE__ */ new Date()).toISOString(),
568373
+ ledgerVersion: 1
568374
+ }) + "\n",
568375
+ "utf-8"
568376
+ );
568377
+ } catch {
568378
+ }
568268
568379
  const hashToIndex = /* @__PURE__ */ new Map();
568269
568380
  for (let i2 = 0; i2 < ctx3.entries.length; i2++) {
568270
568381
  const existing = ctx3.entries[i2];
@@ -568316,7 +568427,7 @@ function saveSessionContext(repoRoot, entry) {
568316
568427
  try {
568317
568428
  writeFileSync45(
568318
568429
  join104(contextDir, "session-diary.md"),
568319
- renderSessionDiary(ctx3.entries.slice(-10)),
568430
+ renderSessionDiary(ctx3.entries.slice(-MAX_SESSION_DIARY_ENTRIES)),
568320
568431
  "utf-8"
568321
568432
  );
568322
568433
  } catch {
@@ -568744,7 +568855,7 @@ function deleteUsageRecord(kind, value2, repoRoot) {
568744
568855
  remove(join104(repoRoot, OMNIUS_DIR, USAGE_HISTORY_FILE));
568745
568856
  }
568746
568857
  }
568747
- var OMNIUS_DIR, LEGACY_DIRS, SUBDIRS, CONTEXT_FILES, PENDING_TASK_FILE, HANDOFF_FILE, CONTEXT_SAVE_FILE, MAX_CONTEXT_ENTRIES, SAME_TASK_REPLACE_WINDOW_MS, LOCK_TIMEOUT_MS, LOCK_RETRY_MS, LOCK_RETRY_MAX, SESSIONS_DIR, SESSIONS_INDEX, SKIP_DIRS2, HOME_SKIP_DIRS, USAGE_HISTORY_FILE, MAX_HISTORY_RECORDS;
568858
+ var OMNIUS_DIR, LEGACY_DIRS, SUBDIRS, CONTEXT_FILES, PENDING_TASK_FILE, HANDOFF_FILE, CONTEXT_SAVE_FILE, CONTEXT_LEDGER_FILE, MAX_CONTEXT_ENTRIES, MAX_SESSION_DIARY_ENTRIES, SAME_TASK_REPLACE_WINDOW_MS, LOCK_TIMEOUT_MS, LOCK_RETRY_MS, LOCK_RETRY_MAX, SESSIONS_DIR, SESSIONS_INDEX, SKIP_DIRS2, HOME_SKIP_DIRS, USAGE_HISTORY_FILE, MAX_HISTORY_RECORDS;
568748
568859
  var init_omnius_directory = __esm({
568749
568860
  "packages/cli/src/tui/omnius-directory.ts"() {
568750
568861
  "use strict";
@@ -568764,7 +568875,9 @@ var init_omnius_directory = __esm({
568764
568875
  PENDING_TASK_FILE = "pending-task.json";
568765
568876
  HANDOFF_FILE = "task-handoff.json";
568766
568877
  CONTEXT_SAVE_FILE = "session-context.json";
568767
- MAX_CONTEXT_ENTRIES = 20;
568878
+ CONTEXT_LEDGER_FILE = "session-context.events.jsonl";
568879
+ MAX_CONTEXT_ENTRIES = 200;
568880
+ MAX_SESSION_DIARY_ENTRIES = 80;
568768
568881
  SAME_TASK_REPLACE_WINDOW_MS = 12 * 60 * 60 * 1e3;
568769
568882
  LOCK_TIMEOUT_MS = 5e3;
568770
568883
  LOCK_RETRY_MS = 50;
@@ -572786,6 +572899,106 @@ ${CONTENT_BG_SEQ}`);
572786
572899
  (seq) => seq.endsWith("m") ? seq : ""
572787
572900
  );
572788
572901
  }
572902
+ reflowContentLines(livePartialLine, width) {
572903
+ const maxWidth = Math.max(16, width);
572904
+ const source = livePartialLine ? [...this._contentLines, livePartialLine] : this._contentLines;
572905
+ return source.flatMap(
572906
+ (line, idx) => this.reflowContentLine(line, maxWidth).map((segment) => ({
572907
+ line: segment,
572908
+ bufferIdx: idx
572909
+ }))
572910
+ );
572911
+ }
572912
+ reflowContentLine(line, width) {
572913
+ const visible = stripAnsi(line);
572914
+ if (visible.length <= width) return [line];
572915
+ const continuationIndent = this.hangingIndentForVisibleLine(
572916
+ visible,
572917
+ width
572918
+ );
572919
+ const ranges = this.visibleWrapRanges(
572920
+ visible,
572921
+ width,
572922
+ continuationIndent.length
572923
+ );
572924
+ if (ranges.length <= 1) return [line];
572925
+ return ranges.map((range, idx) => {
572926
+ const raw = this.rawSliceForVisibleRange(line, range.start, range.end);
572927
+ return idx === 0 ? raw : continuationIndent + raw.trimStart();
572928
+ });
572929
+ }
572930
+ hangingIndentForVisibleLine(visible, width) {
572931
+ const capped = (n2) => " ".repeat(Math.max(0, Math.min(n2, width - 4)));
572932
+ const patterns = [
572933
+ /^(\s*(?:[│├└]\s*)?(?:[-*+○•]\s+))/,
572934
+ /^(\s*(?:[│├└]\s*)?(?:\d+[.)]\s+))/,
572935
+ /^(\s*(?:[│├└]\s*)?(?:>\s*))/,
572936
+ /^(\s*(?:[│├└]\s*)?(?:[A-Za-z][\w.-]{0,28}:\s+))/
572937
+ ];
572938
+ for (const pattern of patterns) {
572939
+ const match = visible.match(pattern);
572940
+ if (match?.[1]) return capped(match[1].length);
572941
+ }
572942
+ return capped(visible.match(/^\s*/)?.[0].length ?? 0);
572943
+ }
572944
+ visibleWrapRanges(visible, width, continuationIndent) {
572945
+ const ranges = [];
572946
+ let start2 = 0;
572947
+ let available = width;
572948
+ while (start2 < visible.length) {
572949
+ if (visible.length - start2 <= available) {
572950
+ ranges.push({ start: start2, end: visible.length });
572951
+ break;
572952
+ }
572953
+ const limit = start2 + available;
572954
+ let breakAt = visible.lastIndexOf(" ", limit);
572955
+ if (breakAt <= start2 + 2) breakAt = limit;
572956
+ let end = breakAt;
572957
+ while (end > start2 && /\s/.test(visible[end - 1] ?? "")) end--;
572958
+ ranges.push({ start: start2, end: Math.max(start2 + 1, end) });
572959
+ start2 = breakAt;
572960
+ while (start2 < visible.length && /\s/.test(visible[start2] ?? "")) start2++;
572961
+ available = Math.max(8, width - continuationIndent);
572962
+ }
572963
+ return ranges;
572964
+ }
572965
+ rawSliceForVisibleRange(line, start2, end) {
572966
+ const startRaw = this.rawIndexForVisibleColumn(line, start2);
572967
+ const endRaw = this.rawIndexForVisibleColumn(line, end);
572968
+ const activeStyle = this.activeSgrAt(line, startRaw);
572969
+ const raw = line.slice(startRaw, endRaw);
572970
+ return activeStyle ? `${activeStyle}${raw}${RESET2}` : raw;
572971
+ }
572972
+ rawIndexForVisibleColumn(line, target) {
572973
+ if (target <= 0) return 0;
572974
+ let visible = 0;
572975
+ for (let i2 = 0; i2 < line.length; i2++) {
572976
+ if (line.charCodeAt(i2) === 27) {
572977
+ const match = line.slice(i2).match(/^\x1B\[[0-?]*[ -/]*[@-~]/);
572978
+ if (match) {
572979
+ i2 += match[0].length - 1;
572980
+ continue;
572981
+ }
572982
+ }
572983
+ if (visible >= target) return i2;
572984
+ visible++;
572985
+ }
572986
+ return line.length;
572987
+ }
572988
+ activeSgrAt(line, rawIndex) {
572989
+ let active = "";
572990
+ const sgr = /\x1B\[[0-9;]*m/g;
572991
+ let match;
572992
+ while ((match = sgr.exec(line)) && match.index < rawIndex) {
572993
+ const seq = match[0];
572994
+ if (seq === RESET2 || /\x1B\[(?:0|39|49)(?:;0)?m/.test(seq)) {
572995
+ active = "";
572996
+ } else {
572997
+ active += seq;
572998
+ }
572999
+ }
573000
+ return active;
573001
+ }
572789
573002
  /**
572790
573003
  * Remove the last N lines from the content scrollback buffer and repaint.
572791
573004
  * Used by Esc-to-recall to erase the just-rendered user prompt.
@@ -572876,9 +573089,10 @@ ${CONTENT_BG_SEQ}`);
572876
573089
  repaintContent() {
572877
573090
  const h = this.contentHeight;
572878
573091
  const livePartialLine = this.getLiveBufferedLine();
572879
- const totalLines = this._contentLines.length + (livePartialLine ? 1 : 0);
572880
- const startIdx = Math.max(0, totalLines - h - this._contentScrollOffset);
572881
573092
  const w = termCols();
573093
+ const reflowedLines = this.reflowContentLines(livePartialLine, w);
573094
+ const totalLines = reflowedLines.length;
573095
+ const startIdx = Math.max(0, totalLines - h - this._contentScrollOffset);
572882
573096
  const headerSafeFloor = layout().headerBottom + 1;
572883
573097
  let buf = "\x1B[?2026h";
572884
573098
  buf += "\x1B7";
@@ -572886,16 +573100,12 @@ ${CONTENT_BG_SEQ}`);
572886
573100
  const selRanges = this._textSelection.getSelectedRanges();
572887
573101
  for (let row = 0; row < h; row++) {
572888
573102
  const lineIdx = startIdx + row;
572889
- let line = "";
572890
- if (lineIdx < this._contentLines.length) {
572891
- line = this._contentLines[lineIdx];
572892
- } else if (livePartialLine && lineIdx === this._contentLines.length) {
572893
- line = livePartialLine;
572894
- }
573103
+ const reflowed = reflowedLines[lineIdx];
573104
+ let line = reflowed?.line ?? "";
572895
573105
  const screenRow = this.scrollRegionTop + row;
572896
573106
  if (screenRow < headerSafeFloor) continue;
572897
573107
  line = line.replace(/\x1B\[0m/g, `\x1B[0m${CONTENT_BG_SEQ}`);
572898
- const sel = selRanges.find((r2) => r2.bufferIdx === lineIdx);
573108
+ const sel = selRanges.find((r2) => r2.bufferIdx === reflowed?.bufferIdx);
572899
573109
  if (sel) {
572900
573110
  line = TextSelection.applyHighlight(line, sel.startCol, sel.endCol);
572901
573111
  }
@@ -575667,7 +575877,7 @@ __export(setup_exports, {
575667
575877
  import * as readline from "node:readline";
575668
575878
  import { execSync as execSync50, spawn as spawn26, exec as exec4 } from "node:child_process";
575669
575879
  import { promisify as promisify6 } from "node:util";
575670
- import { existsSync as existsSync90, writeFileSync as writeFileSync47, readFileSync as readFileSync74, appendFileSync as appendFileSync4, mkdirSync as mkdirSync52 } from "node:fs";
575880
+ import { existsSync as existsSync90, writeFileSync as writeFileSync47, readFileSync as readFileSync74, appendFileSync as appendFileSync6, mkdirSync as mkdirSync52 } from "node:fs";
575671
575881
  import { join as join107 } from "node:path";
575672
575882
  import { homedir as homedir34, platform as platform5 } from "node:os";
575673
575883
  function wrapText(value2, width) {
@@ -578034,7 +578244,7 @@ function ensurePathInShellRc(binDir) {
578034
578244
  const exportLine = `
578035
578245
  export PATH="${binDir}:$PATH" # Added by omnius for nvim
578036
578246
  `;
578037
- appendFileSync4(rcFile, exportLine, "utf8");
578247
+ appendFileSync6(rcFile, exportLine, "utf8");
578038
578248
  console.log(` Added ${binDir} to ${rcFile}`);
578039
578249
  } catch {
578040
578250
  }
@@ -587604,7 +587814,7 @@ import {
587604
587814
  lstatSync,
587605
587815
  statSync as statSync37,
587606
587816
  rmSync as rmSync4,
587607
- appendFileSync as appendFileSync5
587817
+ appendFileSync as appendFileSync7
587608
587818
  } from "node:fs";
587609
587819
  import { relative as relative11, join as join114 } from "node:path";
587610
587820
  async function _immediateReregister(newUrl) {
@@ -592004,7 +592214,7 @@ sleep 1
592004
592214
  const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
592005
592215
  `;
592006
592216
  try {
592007
- appendFileSync5(_spLogFile, line);
592217
+ appendFileSync7(_spLogFile, line);
592008
592218
  } catch {
592009
592219
  }
592010
592220
  };
@@ -592967,12 +593177,16 @@ sleep 1
592967
593177
  return "handled";
592968
593178
  }
592969
593179
  if (requested !== "auto" && requested !== "chat" && requested !== "action") {
592970
- renderWarning("Usage: /telegram mode auto|chat|action [--local]");
593180
+ renderWarning("Usage: /telegram mode auto|chat|action [--global]");
592971
593181
  return "handled";
592972
593182
  }
592973
- ctx3.saveTelegramSettings?.({ mode: requested, local: isLocal });
593183
+ const hasGlobalFlagMode = parts.includes("--global");
593184
+ const projectRootMode = ctx3.repoRoot || process.cwd();
593185
+ const projectHasOmniusMode = existsSync100(join114(projectRootMode, ".omnius"));
593186
+ const wantsLocalMode = hasGlobalFlagMode ? false : isLocal || projectHasOmniusMode;
593187
+ ctx3.saveTelegramSettings?.({ mode: requested, local: wantsLocalMode });
592974
593188
  ctx3.telegramSetInteractionMode?.(requested);
592975
- renderInfo(`Telegram interaction mode set to ${c3.bold(requested)}${isLocal ? " (project)" : " (global)"}.`);
593189
+ renderInfo(`Telegram interaction mode set to ${c3.bold(requested)}${wantsLocalMode ? " (project)" : " (global)"}.`);
592976
593190
  return "handled";
592977
593191
  }
592978
593192
  if (parts[0] === "auth" || parts[0] === "authenticate") {
@@ -593219,18 +593433,23 @@ sleep 1
593219
593433
  }
593220
593434
  const keyIdx = parts.indexOf("--key");
593221
593435
  const adminIdx = parts.indexOf("--admin");
593436
+ const hasGlobalFlag = parts.includes("--global");
593437
+ const projectRoot = ctx3.repoRoot || process.cwd();
593438
+ const projectHasOmnius = existsSync100(join114(projectRoot, ".omnius"));
593439
+ const wantsLocal = hasGlobalFlag ? false : isLocal || projectHasOmnius;
593222
593440
  if (keyIdx !== -1 || adminIdx !== -1) {
593223
593441
  const settings = {
593224
- local: isLocal
593442
+ local: wantsLocal
593225
593443
  };
593226
- const scope = isLocal ? "project" : "global";
593444
+ const scope = wantsLocal ? "project" : "global";
593227
593445
  if (keyIdx !== -1) {
593228
593446
  const token = parts[keyIdx + 1];
593229
593447
  if (!token || token.startsWith("--")) {
593230
593448
  renderWarning(
593231
- "Usage: /telegram --key <bot-token> [--admin <id>] [--local]"
593449
+ "Usage: /telegram --key <bot-token> [--admin <id>] [--global]"
593232
593450
  );
593233
593451
  renderInfo("Get a bot token from @BotFather on Telegram.");
593452
+ renderInfo("Default scope: project-local when .omnius/ exists; use --global to override.");
593234
593453
  return "handled";
593235
593454
  }
593236
593455
  settings.key = token;
@@ -593239,7 +593458,7 @@ sleep 1
593239
593458
  const userId = parts[adminIdx + 1];
593240
593459
  if (!userId || userId.startsWith("--")) {
593241
593460
  renderWarning(
593242
- "Usage: /telegram --admin <user-id-or-username> [--key <token>] [--local]"
593461
+ "Usage: /telegram --admin <user-id-or-username> [--key <token>] [--global]"
593243
593462
  );
593244
593463
  return "handled";
593245
593464
  }
@@ -593254,6 +593473,11 @@ sleep 1
593254
593473
  renderInfo(
593255
593474
  `Telegram admin set to ${c3.bold(settings.admin)} (${scope}).`
593256
593475
  );
593476
+ if (wantsLocal && !projectHasOmnius) {
593477
+ renderWarning(
593478
+ "No .omnius/ in the current directory; this 'local' setting was written to ./.omnius/settings.json and only applies when omnius is invoked from this folder."
593479
+ );
593480
+ }
593257
593481
  if (!ctx3.isTelegramActive?.() && settings.key)
593258
593482
  renderInfo("Use /telegram to start.");
593259
593483
  return "handled";
@@ -593267,9 +593491,30 @@ sleep 1
593267
593491
  if (!settings.key) {
593268
593492
  renderWarning("No Telegram bot token configured.");
593269
593493
  renderInfo("Set one first: /telegram --key <bot-token>");
593270
- renderInfo("Get a token from @BotFather on Telegram.");
593494
+ renderInfo("Get a token from @BotFather on Telegram. The token saves project-locally by default when .omnius/ exists.");
593271
593495
  return "handled";
593272
593496
  }
593497
+ try {
593498
+ const startProjectRoot = ctx3.repoRoot || process.cwd();
593499
+ if (existsSync100(join114(startProjectRoot, ".omnius"))) {
593500
+ const projectSettingsPath = join114(startProjectRoot, ".omnius", "settings.json");
593501
+ let projectHasKey = false;
593502
+ if (existsSync100(projectSettingsPath)) {
593503
+ try {
593504
+ const projectJson = JSON.parse(readFileSync81(projectSettingsPath, "utf8"));
593505
+ projectHasKey = typeof projectJson?.telegramKey === "string" && projectJson.telegramKey.length > 0;
593506
+ } catch {
593507
+ }
593508
+ }
593509
+ if (!projectHasKey) {
593510
+ renderWarning(
593511
+ "Using GLOBAL Telegram token in a project that has no local key. Another omnius instance running with the same token will steal getUpdates and you'll see 409 conflicts."
593512
+ );
593513
+ renderInfo("Scope it locally: /telegram --key <token> (writes to ./.omnius/settings.json)");
593514
+ }
593515
+ }
593516
+ } catch {
593517
+ }
593273
593518
  try {
593274
593519
  await ctx3.telegramStart?.(settings.key, settings.admin);
593275
593520
  } catch (err) {
@@ -593281,8 +593526,8 @@ sleep 1
593281
593526
  }
593282
593527
  renderWarning(`Unknown argument: "${arg}"`);
593283
593528
  renderInfo("Usage:");
593284
- renderInfo(" /telegram --key <token> Save bot token (global)");
593285
- renderInfo(" /telegram --admin <id> Set admin filter (global)");
593529
+ renderInfo(" /telegram --key <token> Save bot token (project-local when .omnius/ exists)");
593530
+ renderInfo(" /telegram --admin <id> Set admin filter (project-local by default)");
593286
593531
  renderInfo(" /telegram Toggle on/off");
593287
593532
  renderInfo(" /telegram stop Stop bridge");
593288
593533
  renderInfo(" /telegram status Show status");
@@ -593301,7 +593546,7 @@ sleep 1
593301
593546
  renderInfo(" /telegram delete-messages <chat> <msg,msg> [reason] Delete multiple messages");
593302
593547
  renderInfo(" /telegram delete-reaction <chat> <msg> --user <id> Delete a reaction");
593303
593548
  renderInfo(" /telegram delete-reactions <chat> --user <id> Delete recent reactions");
593304
- renderInfo(" Add --local to scope settings to this project only");
593549
+ renderInfo(" Add --global to write to ~/.omnius/ (shared across all omnius instances)");
593305
593550
  return "handled";
593306
593551
  }
593307
593552
  case "platforms":
@@ -602934,10 +603179,13 @@ var init_stream_renderer = __esm({
602934
603179
  const words = text.split(/(\s+)/);
602935
603180
  const lines = [];
602936
603181
  let currentLine = "";
603182
+ const continuationIndent = this.hangingIndentForText(text, maxWidth);
603183
+ let available = maxWidth;
602937
603184
  for (const segment of words) {
602938
- if (currentLine.length + segment.length > maxWidth && currentLine.trim()) {
603185
+ if (currentLine.length + segment.length > available && currentLine.trim()) {
602939
603186
  lines.push(currentLine.trimEnd());
602940
- currentLine = segment.replace(/^\s+/, "");
603187
+ currentLine = continuationIndent + segment.replace(/^\s+/, "");
603188
+ available = maxWidth;
602941
603189
  } else {
602942
603190
  currentLine += segment;
602943
603191
  }
@@ -602947,12 +603195,26 @@ var init_stream_renderer = __esm({
602947
603195
  }
602948
603196
  return lines.length > 0 ? lines : [text];
602949
603197
  }
603198
+ hangingIndentForText(text, maxWidth) {
603199
+ const capped = (n2) => " ".repeat(Math.max(0, Math.min(n2, maxWidth - 4)));
603200
+ const patterns = [
603201
+ /^(\s*(?:[-*+○•]\s+))/,
603202
+ /^(\s*(?:\d+[.)]\s+))/,
603203
+ /^(\s*(?:>\s*))/,
603204
+ /^(\s*(?:[A-Za-z][\w.-]{0,28}:\s+))/
603205
+ ];
603206
+ for (const pattern of patterns) {
603207
+ const match = text.match(pattern);
603208
+ if (match?.[1]) return capped(match[1].length);
603209
+ }
603210
+ return capped(text.match(/^\s*/)?.[0].length ?? 0);
603211
+ }
602950
603212
  };
602951
603213
  }
602952
603214
  });
602953
603215
 
602954
603216
  // packages/cli/src/tui/edit-history.ts
602955
- import { appendFileSync as appendFileSync6, mkdirSync as mkdirSync60 } from "node:fs";
603217
+ import { appendFileSync as appendFileSync8, mkdirSync as mkdirSync60 } from "node:fs";
602956
603218
  import { join as join119 } from "node:path";
602957
603219
  function createEditHistoryLogger(repoRoot, sessionId) {
602958
603220
  const historyDir = join119(repoRoot, ".omnius", "history");
@@ -602973,7 +603235,7 @@ function createEditHistoryLogger(repoRoot, sessionId) {
602973
603235
  args: sanitizeArgs(toolName, toolArgs)
602974
603236
  };
602975
603237
  try {
602976
- appendFileSync6(logPath3, JSON.stringify(entry) + "\n", "utf-8");
603238
+ appendFileSync8(logPath3, JSON.stringify(entry) + "\n", "utf-8");
602977
603239
  } catch {
602978
603240
  }
602979
603241
  }
@@ -608356,6 +608618,9 @@ function upsertTelegramReflectionMessage(repoRoot, sessionKey, entry, options2)
608356
608618
  content,
608357
608619
  labels: [entry.mode, entry.mediaSummary, senderLabel(entry)].filter((value2) => Boolean(value2))
608358
608620
  });
608621
+ if (typeof entry.ts === "number" && Number.isFinite(entry.ts) && result.episodeId) {
608622
+ store2.getDb().prepare("UPDATE episodes SET timestamp = ? WHERE id = ?").run(entry.ts, result.episodeId);
608623
+ }
608359
608624
  addTextEdges(graph, result, entry);
608360
608625
  const after = store2.count(sessionKey);
608361
608626
  return { episodeId: result.episodeId, reused: after === before };
@@ -608418,6 +608683,9 @@ async function buildTelegramReflectionCorpus(options2) {
608418
608683
  content,
608419
608684
  labels: [entry.mode, entry.mediaSummary, senderLabel(entry)].filter((value2) => Boolean(value2))
608420
608685
  });
608686
+ if (typeof entry.ts === "number" && Number.isFinite(entry.ts) && result.episodeId) {
608687
+ store2.getDb().prepare("UPDATE episodes SET timestamp = ? WHERE id = ?").run(entry.ts, result.episodeId);
608688
+ }
608421
608689
  addTextEdges(graph, result, entry);
608422
608690
  const after = store2.count(options2.sessionKey);
608423
608691
  if (after === before) reusedEpisodes++;
@@ -609061,7 +609329,7 @@ var init_vision_ingress = __esm({
609061
609329
  });
609062
609330
 
609063
609331
  // packages/cli/src/tui/telegram-bridge.ts
609064
- import { mkdirSync as mkdirSync65, existsSync as existsSync112, unlinkSync as unlinkSync22, readdirSync as readdirSync40, statSync as statSync39, statfsSync as statfsSync5, readFileSync as readFileSync92, writeFileSync as writeFileSync59 } from "node:fs";
609332
+ import { mkdirSync as mkdirSync65, existsSync as existsSync112, unlinkSync as unlinkSync22, readdirSync as readdirSync40, statSync as statSync39, statfsSync as statfsSync5, readFileSync as readFileSync92, writeFileSync as writeFileSync59, appendFileSync as appendFileSync9 } from "node:fs";
609065
609333
  import { join as join127, resolve as resolve43, basename as basename27, relative as relative13, isAbsolute as isAbsolute8, extname as extname16 } from "node:path";
609066
609334
  import { writeFile as writeFileAsync } from "node:fs/promises";
609067
609335
  import { createHash as createHash23, randomBytes as randomBytes22, randomInt } from "node:crypto";
@@ -610277,7 +610545,7 @@ function renderTelegramSubAgentError(username, error) {
610277
610545
  process.stdout.write(` ${c3.dim("│")} ${c3.red("✘")} @${username}: ${c3.dim(preview)}
610278
610546
  `);
610279
610547
  }
610280
- var TELEGRAM_TOOL_ACTION_GROUPS, TELEGRAM_TOOL_ACTION_GROUP, TELEGRAM_TOOL_MUTATING_GROUPS, DEFAULT_TELEGRAM_TOOL_GROUP_POLICY, TELEGRAM_TOOL_BUTTON_LABELS, TELEGRAM_SAFETY_PROMPT, ADMIN_DM_PROMPT, ADMIN_GROUP_PROMPT, TELEGRAM_PUBLIC_SOUL_PROFILE, TELEGRAM_PUBLIC_ORCHESTRATOR_CONTRACT, TELEGRAM_PUBLIC_MEMORY_SCOPE_CONTRACT, TELEGRAM_PUBLIC_VISION_STACK_CONTRACT, GROUP_REPLY_DISCRETION_PROMPT, TELEGRAM_CHAT_MODE_PROMPT, ADMIN_CHAT_PROFILE_PROMPT, TELEGRAM_ACTION_RESPONSE_CONTRACT, TELEGRAM_EXTERNAL_ACQUISITION_CONTRACT, TELEGRAM_STUCK_SELF_TALK_PREFIXES, TELEGRAM_CHAT_HISTORY_LIMIT, TELEGRAM_CONTEXT_RECENT_DEFAULT, TELEGRAM_CONTEXT_LINE_LIMIT, TELEGRAM_CONTEXT_SAMPLE_LIMIT, TELEGRAM_MEMORY_CARD_LIMIT, TELEGRAM_MEMORY_NOTE_LIMIT, TELEGRAM_MEMORY_STOPWORDS, TELEGRAM_SUB_AGENT_BOUNDED_OPTIONS, TELEGRAM_PUBLIC_HELP_COMMANDS, TELEGRAM_REMINDER_SLASH_COMMANDS, TELEGRAM_REFLECTION_SLASH_COMMANDS, TELEGRAM_IMAGE_EXTENSIONS, MEDIA_CACHE_TTL_MS, TELEGRAM_CHANNEL_DMN_SWEEP_MS, TELEGRAM_CHANNEL_DMN_IDLE_AFTER_MS, TELEGRAM_CHANNEL_DMN_MIN_INTERVAL_MS, TELEGRAM_CHANNEL_DMN_MIN_MESSAGES, TELEGRAM_PUBLIC_TOOL_QUOTAS, TelegramBridge;
610548
+ var TELEGRAM_TOOL_ACTION_GROUPS, TELEGRAM_TOOL_ACTION_GROUP, TELEGRAM_TOOL_MUTATING_GROUPS, DEFAULT_TELEGRAM_TOOL_GROUP_POLICY, TELEGRAM_TOOL_BUTTON_LABELS, TELEGRAM_SAFETY_PROMPT, ADMIN_DM_PROMPT, ADMIN_GROUP_PROMPT, TELEGRAM_PUBLIC_SOUL_PROFILE, TELEGRAM_PUBLIC_ORCHESTRATOR_CONTRACT, TELEGRAM_PUBLIC_MEMORY_SCOPE_CONTRACT, TELEGRAM_PUBLIC_VISION_STACK_CONTRACT, GROUP_REPLY_DISCRETION_PROMPT, TELEGRAM_CHAT_MODE_PROMPT, ADMIN_CHAT_PROFILE_PROMPT, TELEGRAM_ACTION_RESPONSE_CONTRACT, TELEGRAM_EXTERNAL_ACQUISITION_CONTRACT, TELEGRAM_STUCK_SELF_TALK_PREFIXES, TELEGRAM_CHAT_HISTORY_LIMIT, TELEGRAM_CONTEXT_RECENT_DEFAULT, TELEGRAM_CONTEXT_LINE_LIMIT, TELEGRAM_CONTEXT_SAMPLE_LIMIT, TELEGRAM_MEMORY_CARD_LIMIT, TELEGRAM_MEMORY_NOTE_LIMIT, TELEGRAM_ASSOCIATIVE_FACT_LIMIT, TELEGRAM_ASSOCIATIVE_USER_FACT_LIMIT, TELEGRAM_ASSOCIATIVE_ACTION_LIMIT, TELEGRAM_ASSOCIATIVE_RELATION_LIMIT, TELEGRAM_MEMORY_STOPWORDS, TELEGRAM_SUB_AGENT_BOUNDED_OPTIONS, TELEGRAM_PUBLIC_HELP_COMMANDS, TELEGRAM_REMINDER_SLASH_COMMANDS, TELEGRAM_REFLECTION_SLASH_COMMANDS, TELEGRAM_IMAGE_EXTENSIONS, MEDIA_CACHE_TTL_MS, TELEGRAM_CHANNEL_DMN_SWEEP_MS, TELEGRAM_CHANNEL_DMN_IDLE_AFTER_MS, TELEGRAM_CHANNEL_DMN_MIN_INTERVAL_MS, TELEGRAM_CHANNEL_DMN_MIN_MESSAGES, TELEGRAM_PUBLIC_TOOL_QUOTAS, TelegramBridge;
610281
610549
  var init_telegram_bridge = __esm({
610282
610550
  "packages/cli/src/tui/telegram-bridge.ts"() {
610283
610551
  "use strict";
@@ -610296,7 +610564,9 @@ var init_telegram_bridge = __esm({
610296
610564
  init_visual_identity_association();
610297
610565
  init_telegram_channel_dmn();
610298
610566
  init_telegram_reflection_corpus();
610567
+ init_memory_paths();
610299
610568
  init_telegram_reflection_extraction();
610569
+ init_dist7();
610300
610570
  TELEGRAM_TOOL_ACTION_GROUPS = [
610301
610571
  "read",
610302
610572
  "message",
@@ -610458,6 +610728,7 @@ PUBLIC TELEGRAM MEMORY SCOPE
610458
610728
 
610459
610729
  This turn may use memory and conversation history for the current Telegram group/private chat scope only.
610460
610730
  Users in a shared public group may ask questions about that shared group history and group memory, scoped by the current group id or by a user id/username inside that same group.
610731
+ Durable associative memory, participant profiles, relationships, and action ledgers persist on disk across Omnius terminal sessions and package updates. Use those recalled facts when the current sender or topic matches them; treat user assertions as scoped memory evidence, not universal truth.
610461
610732
  Private chats, admin DMs, other groups, local terminal sessions, and fragmented private contexts are not visible from this public group. Do not imply they exist and do not answer from them.
610462
610733
  `.trim();
610463
610734
  TELEGRAM_PUBLIC_VISION_STACK_CONTRACT = `
@@ -610538,12 +610809,16 @@ External acquisition contract:
610538
610809
  /^hmm,?\s+(let me|maybe|wait)\b/i,
610539
610810
  /^ok(ay)?,?\s+let me try\b/i
610540
610811
  ];
610541
- TELEGRAM_CHAT_HISTORY_LIMIT = 240;
610812
+ TELEGRAM_CHAT_HISTORY_LIMIT = 5e3;
610542
610813
  TELEGRAM_CONTEXT_RECENT_DEFAULT = 36;
610543
610814
  TELEGRAM_CONTEXT_LINE_LIMIT = 320;
610544
610815
  TELEGRAM_CONTEXT_SAMPLE_LIMIT = 10;
610545
- TELEGRAM_MEMORY_CARD_LIMIT = 80;
610546
- TELEGRAM_MEMORY_NOTE_LIMIT = 8;
610816
+ TELEGRAM_MEMORY_CARD_LIMIT = 240;
610817
+ TELEGRAM_MEMORY_NOTE_LIMIT = 24;
610818
+ TELEGRAM_ASSOCIATIVE_FACT_LIMIT = 600;
610819
+ TELEGRAM_ASSOCIATIVE_USER_FACT_LIMIT = 80;
610820
+ TELEGRAM_ASSOCIATIVE_ACTION_LIMIT = 5e3;
610821
+ TELEGRAM_ASSOCIATIVE_RELATION_LIMIT = 300;
610547
610822
  TELEGRAM_MEMORY_STOPWORDS = /* @__PURE__ */ new Set([
610548
610823
  "about",
610549
610824
  "after",
@@ -610627,6 +610902,7 @@ External acquisition contract:
610627
610902
  this.telegramToolPolicy = resolveSettings(repoRoot || ".").telegramToolPolicy ?? {};
610628
610903
  this.mediaCacheDir = resolve43(repoRoot || ".", ".omnius", "telegram-media-cache");
610629
610904
  this.telegramConversationDir = resolve43(repoRoot || ".", ".omnius", "telegram-conversations");
610905
+ this.telegramSqlitePath = resolve43(repoRoot || ".", ".omnius", "telegram.sqlite");
610630
610906
  this.telegramToolButtonDir = resolve43(repoRoot || ".", ".omnius", "telegram-tool-buttons");
610631
610907
  }
610632
610908
  botToken;
@@ -610658,6 +610934,8 @@ External acquisition contract:
610658
610934
  chatParticipants = /* @__PURE__ */ new Map();
610659
610935
  /** Lightweight Zettelkasten-style memory cards by chat/guest session key */
610660
610936
  chatMemoryCards = /* @__PURE__ */ new Map();
610937
+ /** Durable associative memory by scoped Telegram chat key. */
610938
+ chatAssociativeMemory = /* @__PURE__ */ new Map();
610661
610939
  /** Generic chronological attention cadence shared by live surfaces. */
610662
610940
  stimulation = new StimulationController();
610663
610941
  /** Throttles noisy "skipped group chatter" waterfall logs */
@@ -610707,6 +610985,15 @@ External acquisition contract:
610707
610985
  mediaCacheDir;
610708
610986
  /** Persistent conversation memory directory */
610709
610987
  telegramConversationDir;
610988
+ /** Durable SQLite mirror for raw Telegram messages and metadata. */
610989
+ telegramSqlitePath;
610990
+ telegramSqliteDb = null;
610991
+ /**
610992
+ * Per-bot ownership lockfile under .omnius/telegram-runner-state/.
610993
+ * Prevents two omnius instances from polling the same bot token concurrently
610994
+ * (which would cause silent 409 conflicts on getUpdates). Released in stop().
610995
+ */
610996
+ telegramOwnerLockFile = null;
610710
610997
  /** Session keys loaded from persistent conversation memory */
610711
610998
  loadedConversationState = /* @__PURE__ */ new Set();
610712
610999
  /** True once persisted Telegram conversation scopes have been bulk-loaded. */
@@ -610993,12 +611280,26 @@ No scoped reflection artifact exists yet for this chat. Use <code>/reflect</code
610993
611280
  }
610994
611281
  recordChatHistory(sessionKey, entry) {
610995
611282
  this.ensureTelegramConversationLoaded(sessionKey);
611283
+ const stamped = { ...entry, ts: entry.ts ?? Date.now() };
610996
611284
  const existing = this.chatHistory.get(sessionKey) ?? [];
610997
- existing.push({ ...entry, ts: entry.ts ?? Date.now() });
611285
+ existing.push(stamped);
610998
611286
  if (existing.length > TELEGRAM_CHAT_HISTORY_LIMIT) {
610999
611287
  existing.splice(0, existing.length - TELEGRAM_CHAT_HISTORY_LIMIT);
611000
611288
  }
611001
611289
  this.chatHistory.set(sessionKey, existing);
611290
+ this.appendTelegramConversationLedger(sessionKey, stamped);
611291
+ }
611292
+ appendTelegramConversationLedger(sessionKey, entry) {
611293
+ if (!this.repoRoot) return;
611294
+ try {
611295
+ mkdirSync65(this.telegramConversationDir, { recursive: true });
611296
+ appendFileSync9(
611297
+ this.telegramConversationLedgerPath(sessionKey),
611298
+ JSON.stringify({ sessionKey, ...entry }) + "\n",
611299
+ "utf8"
611300
+ );
611301
+ } catch {
611302
+ }
611002
611303
  }
611003
611304
  telegramReplySenderWithSelfFlag(sender) {
611004
611305
  if (!sender) return void 0;
@@ -611226,6 +611527,272 @@ ${mediaContext}` : ""
611226
611527
  const safe = createHash23("sha1").update(sessionKey).digest("hex").slice(0, 20);
611227
611528
  return join127(this.telegramConversationDir, `${safe}.json`);
611228
611529
  }
611530
+ telegramConversationLedgerPath(sessionKey) {
611531
+ const safe = createHash23("sha1").update(sessionKey).digest("hex").slice(0, 20);
611532
+ return join127(this.telegramConversationDir, `${safe}.events.jsonl`);
611533
+ }
611534
+ telegramDb() {
611535
+ if (this.telegramSqliteDb === false) return null;
611536
+ if (this.telegramSqliteDb) return this.telegramSqliteDb;
611537
+ if (!this.repoRoot) {
611538
+ this.telegramSqliteDb = false;
611539
+ return null;
611540
+ }
611541
+ try {
611542
+ mkdirSync65(resolve43(this.repoRoot, ".omnius"), { recursive: true });
611543
+ const db = initDb(this.telegramSqlitePath);
611544
+ db.exec(`
611545
+ CREATE TABLE IF NOT EXISTS telegram_messages (
611546
+ session_key TEXT NOT NULL,
611547
+ chat_id TEXT NOT NULL,
611548
+ chat_type TEXT,
611549
+ chat_title TEXT,
611550
+ message_id INTEGER NOT NULL,
611551
+ message_thread_id INTEGER,
611552
+ update_id INTEGER,
611553
+ telegram_date INTEGER,
611554
+ received_at INTEGER NOT NULL,
611555
+ role TEXT NOT NULL DEFAULT 'user',
611556
+ from_user_id INTEGER,
611557
+ username TEXT,
611558
+ first_name TEXT,
611559
+ text TEXT,
611560
+ raw_json TEXT NOT NULL,
611561
+ normalized_json TEXT NOT NULL,
611562
+ reply_to_message_id INTEGER,
611563
+ reply_to_username TEXT,
611564
+ media_json TEXT,
611565
+ PRIMARY KEY (chat_id, message_id, role)
611566
+ );
611567
+ CREATE INDEX IF NOT EXISTS idx_telegram_messages_session_time ON telegram_messages(session_key, received_at);
611568
+ CREATE INDEX IF NOT EXISTS idx_telegram_messages_user_time ON telegram_messages(session_key, from_user_id, received_at);
611569
+ CREATE INDEX IF NOT EXISTS idx_telegram_messages_username_time ON telegram_messages(session_key, username, received_at);
611570
+ CREATE VIRTUAL TABLE IF NOT EXISTS telegram_messages_fts USING fts5(
611571
+ session_key UNINDEXED,
611572
+ chat_id UNINDEXED,
611573
+ message_id UNINDEXED,
611574
+ username,
611575
+ first_name,
611576
+ text,
611577
+ content='telegram_messages',
611578
+ content_rowid='rowid'
611579
+ );
611580
+ CREATE TRIGGER IF NOT EXISTS telegram_messages_ai AFTER INSERT ON telegram_messages BEGIN
611581
+ INSERT INTO telegram_messages_fts(rowid, session_key, chat_id, message_id, username, first_name, text)
611582
+ VALUES (new.rowid, new.session_key, new.chat_id, new.message_id, new.username, new.first_name, new.text);
611583
+ END;
611584
+ CREATE TRIGGER IF NOT EXISTS telegram_messages_ad AFTER DELETE ON telegram_messages BEGIN
611585
+ INSERT INTO telegram_messages_fts(telegram_messages_fts, rowid, session_key, chat_id, message_id, username, first_name, text)
611586
+ VALUES('delete', old.rowid, old.session_key, old.chat_id, old.message_id, old.username, old.first_name, old.text);
611587
+ END;
611588
+ CREATE TRIGGER IF NOT EXISTS telegram_messages_au AFTER UPDATE ON telegram_messages BEGIN
611589
+ INSERT INTO telegram_messages_fts(telegram_messages_fts, rowid, session_key, chat_id, message_id, username, first_name, text)
611590
+ VALUES('delete', old.rowid, old.session_key, old.chat_id, old.message_id, old.username, old.first_name, old.text);
611591
+ INSERT INTO telegram_messages_fts(rowid, session_key, chat_id, message_id, username, first_name, text)
611592
+ VALUES (new.rowid, new.session_key, new.chat_id, new.message_id, new.username, new.first_name, new.text);
611593
+ END;
611594
+ `);
611595
+ this.telegramSqliteDb = db;
611596
+ return db;
611597
+ } catch {
611598
+ this.telegramSqliteDb = false;
611599
+ return null;
611600
+ }
611601
+ }
611602
+ persistTelegramRawMessage(update2, msg) {
611603
+ const db = this.telegramDb();
611604
+ if (!db) return;
611605
+ const rawMessage = update2.message ?? update2.guest_message ?? update2.edited_message ?? update2.channel_post ?? {};
611606
+ const sessionKey = this.sessionKeyForMessage(msg);
611607
+ const media = msg.media || msg.replyToMedia || msg.livePhoto;
611608
+ try {
611609
+ db.prepare(`
611610
+ INSERT INTO telegram_messages (
611611
+ session_key, chat_id, chat_type, chat_title, message_id, message_thread_id,
611612
+ update_id, telegram_date, received_at, role, from_user_id, username, first_name,
611613
+ text, raw_json, normalized_json, reply_to_message_id, reply_to_username, media_json
611614
+ ) VALUES (
611615
+ @session_key, @chat_id, @chat_type, @chat_title, @message_id, @message_thread_id,
611616
+ @update_id, @telegram_date, @received_at, 'user', @from_user_id, @username, @first_name,
611617
+ @text, @raw_json, @normalized_json, @reply_to_message_id, @reply_to_username, @media_json
611618
+ )
611619
+ ON CONFLICT(chat_id, message_id, role) DO UPDATE SET
611620
+ session_key=excluded.session_key,
611621
+ chat_type=excluded.chat_type,
611622
+ chat_title=excluded.chat_title,
611623
+ message_thread_id=excluded.message_thread_id,
611624
+ update_id=excluded.update_id,
611625
+ telegram_date=excluded.telegram_date,
611626
+ received_at=excluded.received_at,
611627
+ from_user_id=excluded.from_user_id,
611628
+ username=excluded.username,
611629
+ first_name=excluded.first_name,
611630
+ text=excluded.text,
611631
+ raw_json=excluded.raw_json,
611632
+ normalized_json=excluded.normalized_json,
611633
+ reply_to_message_id=excluded.reply_to_message_id,
611634
+ reply_to_username=excluded.reply_to_username,
611635
+ media_json=excluded.media_json
611636
+ `).run({
611637
+ session_key: sessionKey,
611638
+ chat_id: String(msg.chatId),
611639
+ chat_type: msg.chatType,
611640
+ chat_title: msg.chatTitle ?? null,
611641
+ message_id: msg.messageId,
611642
+ message_thread_id: msg.messageThreadId ?? null,
611643
+ update_id: typeof update2.update_id === "number" ? update2.update_id : null,
611644
+ telegram_date: typeof rawMessage.date === "number" ? rawMessage.date : null,
611645
+ received_at: Date.now(),
611646
+ from_user_id: msg.fromUserId ?? null,
611647
+ username: msg.username || null,
611648
+ first_name: msg.firstName || null,
611649
+ text: msg.text || "",
611650
+ raw_json: JSON.stringify(rawMessage),
611651
+ normalized_json: JSON.stringify(msg),
611652
+ reply_to_message_id: msg.replyToMessageId ?? null,
611653
+ reply_to_username: msg.replyToUsername || null,
611654
+ media_json: media ? JSON.stringify(media) : null
611655
+ });
611656
+ } catch {
611657
+ }
611658
+ }
611659
+ persistTelegramAssistantMessage(msg, text, messageId, replyToMessageId) {
611660
+ if (!messageId) return;
611661
+ const db = this.telegramDb();
611662
+ if (!db) return;
611663
+ const sessionKey = this.sessionKeyForMessage(msg);
611664
+ const now = Date.now();
611665
+ const normalized = {
611666
+ role: "assistant",
611667
+ chatId: msg.chatId,
611668
+ chatType: msg.chatType,
611669
+ chatTitle: msg.chatTitle,
611670
+ messageId,
611671
+ messageThreadId: msg.messageThreadId,
611672
+ replyToMessageId,
611673
+ username: this.state.botUsername || "omnius",
611674
+ text
611675
+ };
611676
+ try {
611677
+ db.prepare(`
611678
+ INSERT INTO telegram_messages (
611679
+ session_key, chat_id, chat_type, chat_title, message_id, message_thread_id,
611680
+ update_id, telegram_date, received_at, role, from_user_id, username, first_name,
611681
+ text, raw_json, normalized_json, reply_to_message_id, reply_to_username, media_json
611682
+ ) VALUES (
611683
+ @session_key, @chat_id, @chat_type, @chat_title, @message_id, @message_thread_id,
611684
+ NULL, NULL, @received_at, 'assistant', NULL, @username, 'Omnius',
611685
+ @text, @raw_json, @normalized_json, @reply_to_message_id, NULL, NULL
611686
+ )
611687
+ ON CONFLICT(chat_id, message_id, role) DO UPDATE SET
611688
+ session_key=excluded.session_key,
611689
+ chat_type=excluded.chat_type,
611690
+ chat_title=excluded.chat_title,
611691
+ message_thread_id=excluded.message_thread_id,
611692
+ received_at=excluded.received_at,
611693
+ username=excluded.username,
611694
+ text=excluded.text,
611695
+ raw_json=excluded.raw_json,
611696
+ normalized_json=excluded.normalized_json,
611697
+ reply_to_message_id=excluded.reply_to_message_id
611698
+ `).run({
611699
+ session_key: sessionKey,
611700
+ chat_id: String(msg.chatId),
611701
+ chat_type: msg.chatType,
611702
+ chat_title: msg.chatTitle ?? null,
611703
+ message_id: messageId,
611704
+ message_thread_id: msg.messageThreadId ?? null,
611705
+ received_at: now,
611706
+ username: this.state.botUsername || "omnius",
611707
+ text,
611708
+ raw_json: JSON.stringify(normalized),
611709
+ normalized_json: JSON.stringify(normalized),
611710
+ reply_to_message_id: replyToMessageId ?? null
611711
+ });
611712
+ } catch {
611713
+ }
611714
+ }
611715
+ createTelegramAssociativeMemory() {
611716
+ const now = Date.now();
611717
+ return {
611718
+ version: 1,
611719
+ createdAt: now,
611720
+ updatedAt: now,
611721
+ facts: [],
611722
+ users: {},
611723
+ relationships: [],
611724
+ actions: []
611725
+ };
611726
+ }
611727
+ normalizeTelegramAssociativeMemory(raw) {
611728
+ const created = this.createTelegramAssociativeMemory();
611729
+ const users = {};
611730
+ const rawUsers = raw.users && typeof raw.users === "object" ? raw.users : {};
611731
+ for (const [key, user] of Object.entries(rawUsers)) {
611732
+ if (!user || typeof user !== "object") continue;
611733
+ users[key] = {
611734
+ username: String(user.username || "unknown"),
611735
+ displayName: user.displayName,
611736
+ userId: typeof user.userId === "number" ? user.userId : void 0,
611737
+ aliases: Array.isArray(user.aliases) ? user.aliases.map(String).slice(0, 20) : [],
611738
+ firstSeenTs: typeof user.firstSeenTs === "number" ? user.firstSeenTs : created.createdAt,
611739
+ lastSeenTs: typeof user.lastSeenTs === "number" ? user.lastSeenTs : created.updatedAt,
611740
+ messageCount: typeof user.messageCount === "number" ? user.messageCount : 0,
611741
+ directAddressCount: typeof user.directAddressCount === "number" ? user.directAddressCount : 0,
611742
+ replyCount: typeof user.replyCount === "number" ? user.replyCount : 0,
611743
+ toneTags: Array.isArray(user.toneTags) ? user.toneTags.map(String).slice(0, 20) : [],
611744
+ facts: Array.isArray(user.facts) ? user.facts.slice(0, TELEGRAM_ASSOCIATIVE_USER_FACT_LIMIT).map((fact) => this.normalizeTelegramAssociativeFact(fact)) : [],
611745
+ relationshipHints: Array.isArray(user.relationshipHints) ? user.relationshipHints.map(String).slice(0, 80) : [],
611746
+ recentTopics: Array.isArray(user.recentTopics) ? user.recentTopics.map(String).slice(0, 80) : [],
611747
+ lastMessages: Array.isArray(user.lastMessages) ? user.lastMessages.map(String).slice(-40) : []
611748
+ };
611749
+ }
611750
+ return {
611751
+ version: typeof raw.version === "number" ? raw.version : 1,
611752
+ createdAt: typeof raw.createdAt === "number" ? raw.createdAt : created.createdAt,
611753
+ updatedAt: typeof raw.updatedAt === "number" ? raw.updatedAt : created.updatedAt,
611754
+ facts: Array.isArray(raw.facts) ? raw.facts.slice(0, TELEGRAM_ASSOCIATIVE_FACT_LIMIT).map((fact) => this.normalizeTelegramAssociativeFact(fact)) : [],
611755
+ users,
611756
+ relationships: Array.isArray(raw.relationships) ? raw.relationships.slice(0, TELEGRAM_ASSOCIATIVE_RELATION_LIMIT).map((fact) => this.normalizeTelegramAssociativeFact(fact)) : [],
611757
+ actions: Array.isArray(raw.actions) ? raw.actions.slice(-TELEGRAM_ASSOCIATIVE_ACTION_LIMIT).map((action) => ({
611758
+ id: String(action.id || createHash23("sha1").update(JSON.stringify(action)).digest("hex").slice(0, 12)),
611759
+ ts: typeof action.ts === "number" ? action.ts : Date.now(),
611760
+ role: action.role === "assistant" ? "assistant" : "user",
611761
+ speaker: String(action.speaker || "unknown"),
611762
+ mode: action.mode,
611763
+ text: String(action.text || ""),
611764
+ messageId: typeof action.messageId === "number" ? action.messageId : void 0,
611765
+ replyToMessageId: typeof action.replyToMessageId === "number" ? action.replyToMessageId : void 0,
611766
+ userId: typeof action.userId === "number" ? action.userId : void 0,
611767
+ username: typeof action.username === "string" ? action.username : void 0
611768
+ })) : []
611769
+ };
611770
+ }
611771
+ normalizeTelegramAssociativeFact(raw) {
611772
+ const text = String(raw.text || "").trim();
611773
+ const now = Date.now();
611774
+ return {
611775
+ id: String(raw.id || createHash23("sha1").update(text || String(now)).digest("hex").slice(0, 12)),
611776
+ text,
611777
+ tags: Array.isArray(raw.tags) ? raw.tags.map(String).slice(0, 16) : [],
611778
+ speakers: Array.isArray(raw.speakers) ? raw.speakers.map(String).slice(0, 16) : [],
611779
+ userIds: Array.isArray(raw.userIds) ? raw.userIds.filter((id) => typeof id === "number").slice(0, 32) : [],
611780
+ usernames: Array.isArray(raw.usernames) ? raw.usernames.map(String).slice(0, 32) : [],
611781
+ messageIds: Array.isArray(raw.messageIds) ? raw.messageIds.filter((id) => typeof id === "number").slice(0, 80) : [],
611782
+ createdAt: typeof raw.createdAt === "number" ? raw.createdAt : now,
611783
+ updatedAt: typeof raw.updatedAt === "number" ? raw.updatedAt : now,
611784
+ weight: typeof raw.weight === "number" ? raw.weight : 1
611785
+ };
611786
+ }
611787
+ telegramAssociativeMemoryForSession(sessionKey) {
611788
+ this.ensureTelegramConversationLoaded(sessionKey);
611789
+ let memory = this.chatAssociativeMemory.get(sessionKey);
611790
+ if (!memory) {
611791
+ memory = this.createTelegramAssociativeMemory();
611792
+ this.chatAssociativeMemory.set(sessionKey, memory);
611793
+ }
611794
+ return memory;
611795
+ }
611229
611796
  telegramPersonalityScope(sessionKey, msg) {
611230
611797
  const label = msg.chatType !== "private" ? `${msg.chatTitle || msg.chatType}-${String(msg.chatId)}` : `private-${msg.username || msg.fromUserId || msg.chatId}`;
611231
611798
  return {
@@ -611242,8 +611809,9 @@ ${mediaContext}` : ""
611242
611809
  if (!existsSync112(path12)) return;
611243
611810
  try {
611244
611811
  const parsed = JSON.parse(readFileSync92(path12, "utf8"));
611812
+ const loadedHistory = Array.isArray(parsed.history) ? parsed.history : [];
611245
611813
  if (Array.isArray(parsed.history)) {
611246
- this.chatHistory.set(sessionKey, parsed.history.slice(-TELEGRAM_CHAT_HISTORY_LIMIT));
611814
+ this.chatHistory.set(sessionKey, loadedHistory.slice(-TELEGRAM_CHAT_HISTORY_LIMIT));
611247
611815
  }
611248
611816
  if (Array.isArray(parsed.participants)) {
611249
611817
  const participants = /* @__PURE__ */ new Map();
@@ -611257,8 +611825,28 @@ ${mediaContext}` : ""
611257
611825
  }
611258
611826
  this.chatParticipants.set(sessionKey, participants);
611259
611827
  }
611260
- if (Array.isArray(parsed.memoryCards)) {
611828
+ const cardsLookTruncated = Array.isArray(parsed.memoryCards) && parsed.memoryCards.some((card) => Array.isArray(card.notes) && card.notes.some((note) => typeof note === "string" && note.endsWith("...")));
611829
+ if (Array.isArray(parsed.memoryCards) && !cardsLookTruncated) {
611261
611830
  this.chatMemoryCards.set(sessionKey, parsed.memoryCards.slice(0, TELEGRAM_MEMORY_CARD_LIMIT));
611831
+ } else if (loadedHistory.length > 0) {
611832
+ this.chatMemoryCards.set(sessionKey, []);
611833
+ for (const entry of loadedHistory) {
611834
+ this.updateTelegramMemoryCards(sessionKey, entry);
611835
+ }
611836
+ }
611837
+ const associativeLooksTruncated = parsed.associativeMemory && Array.isArray(parsed.associativeMemory.facts) && parsed.associativeMemory.facts.some(
611838
+ (fact) => typeof fact?.text === "string" && fact.text.endsWith("...")
611839
+ );
611840
+ if (parsed.associativeMemory && !associativeLooksTruncated) {
611841
+ this.chatAssociativeMemory.set(
611842
+ sessionKey,
611843
+ this.normalizeTelegramAssociativeMemory(parsed.associativeMemory)
611844
+ );
611845
+ } else if (loadedHistory.length > 0) {
611846
+ this.hydrateTelegramAssociativeMemoryFromHistory(sessionKey, loadedHistory);
611847
+ }
611848
+ if (loadedHistory.length > 0) {
611849
+ this.backfillTelegramLoadedHistory(sessionKey, loadedHistory);
611262
611850
  }
611263
611851
  if (parsed.stimulation) {
611264
611852
  this.stimulation.setState(sessionKey, parsed.stimulation);
@@ -611273,6 +611861,108 @@ ${mediaContext}` : ""
611273
611861
  } catch {
611274
611862
  }
611275
611863
  }
611864
+ hydrateTelegramAssociativeMemoryFromHistory(sessionKey, history) {
611865
+ const memory = this.createTelegramAssociativeMemory();
611866
+ this.chatAssociativeMemory.set(sessionKey, memory);
611867
+ for (const entry of history) {
611868
+ this.updateTelegramAssociativeMemory(sessionKey, entry);
611869
+ }
611870
+ }
611871
+ telegramHistoryBackfillMessageId(sessionKey, entry, index) {
611872
+ if (typeof entry.messageId === "number" && Number.isFinite(entry.messageId)) return entry.messageId;
611873
+ const digest3 = createHash23("sha1").update(`${sessionKey}:${index}:${entry.role}:${entry.ts ?? ""}:${entry.text}`).digest("hex").slice(0, 8);
611874
+ return -Number.parseInt(digest3, 16);
611875
+ }
611876
+ backfillTelegramLoadedHistory(sessionKey, history) {
611877
+ if (!this.repoRoot || history.length === 0) return;
611878
+ const db = this.telegramDb();
611879
+ if (db) {
611880
+ try {
611881
+ const existing = db.prepare("SELECT COUNT(*) AS n FROM telegram_messages WHERE session_key = ?").get(sessionKey);
611882
+ if ((existing?.n ?? 0) < history.length) {
611883
+ const insert = db.prepare(`
611884
+ INSERT INTO telegram_messages (
611885
+ session_key, chat_id, chat_type, chat_title, message_id, message_thread_id,
611886
+ update_id, telegram_date, received_at, role, from_user_id, username, first_name,
611887
+ text, raw_json, normalized_json, reply_to_message_id, reply_to_username, media_json
611888
+ ) VALUES (
611889
+ @session_key, @chat_id, @chat_type, @chat_title, @message_id, @message_thread_id,
611890
+ NULL, NULL, @received_at, @role, @from_user_id, @username, @first_name,
611891
+ @text, @raw_json, @normalized_json, @reply_to_message_id, @reply_to_username, @media_json
611892
+ )
611893
+ ON CONFLICT(chat_id, message_id, role) DO UPDATE SET
611894
+ session_key=excluded.session_key,
611895
+ chat_type=excluded.chat_type,
611896
+ chat_title=excluded.chat_title,
611897
+ message_thread_id=excluded.message_thread_id,
611898
+ received_at=excluded.received_at,
611899
+ from_user_id=excluded.from_user_id,
611900
+ username=excluded.username,
611901
+ first_name=excluded.first_name,
611902
+ text=excluded.text,
611903
+ raw_json=excluded.raw_json,
611904
+ normalized_json=excluded.normalized_json,
611905
+ reply_to_message_id=excluded.reply_to_message_id,
611906
+ reply_to_username=excluded.reply_to_username,
611907
+ media_json=excluded.media_json
611908
+ `);
611909
+ const transaction = db.transaction((entries) => {
611910
+ entries.forEach((entry, index) => {
611911
+ const messageId = this.telegramHistoryBackfillMessageId(sessionKey, entry, index);
611912
+ const chatId = entry.chatId !== void 0 ? String(entry.chatId) : sessionKey.replace(/^chat:/, "");
611913
+ insert.run({
611914
+ session_key: sessionKey,
611915
+ chat_id: chatId || "unknown",
611916
+ chat_type: entry.chatType || null,
611917
+ chat_title: entry.chatTitle || null,
611918
+ message_id: messageId,
611919
+ message_thread_id: entry.messageThreadId ?? null,
611920
+ received_at: entry.ts ?? Date.now(),
611921
+ role: entry.role === "assistant" ? "assistant" : "user",
611922
+ from_user_id: entry.fromUserId ?? null,
611923
+ username: entry.role === "assistant" ? this.state.botUsername || "omnius" : entry.username || null,
611924
+ first_name: entry.role === "assistant" ? "Omnius" : entry.firstName || null,
611925
+ text: entry.text || "",
611926
+ raw_json: JSON.stringify({ source: "telegram-conversations", sessionKey, entry }),
611927
+ normalized_json: JSON.stringify({ ...entry, messageId }),
611928
+ reply_to_message_id: entry.replyToMessageId ?? null,
611929
+ reply_to_username: entry.replyContext?.sender?.username || null,
611930
+ media_json: entry.mediaSummary ? JSON.stringify({ summary: entry.mediaSummary }) : null
611931
+ });
611932
+ });
611933
+ });
611934
+ transaction(history);
611935
+ try {
611936
+ db.exec("INSERT INTO telegram_messages_fts(telegram_messages_fts) VALUES('rebuild')");
611937
+ } catch {
611938
+ }
611939
+ }
611940
+ } catch {
611941
+ }
611942
+ }
611943
+ try {
611944
+ const paths = omniusMemoryDbPaths(this.repoRoot);
611945
+ const graph = new TemporalGraph(paths.knowledge);
611946
+ const store2 = new EpisodeStore(paths.episodes, graph);
611947
+ let existingTelegramEpisodes = 0;
611948
+ try {
611949
+ const row = store2.getDb().prepare(`
611950
+ SELECT COUNT(*) AS n FROM episodes
611951
+ WHERE session_id = ? AND metadata LIKE '%"sourceSurface":"telegram"%'
611952
+ `).get(sessionKey);
611953
+ existingTelegramEpisodes = row?.n ?? 0;
611954
+ } finally {
611955
+ store2.close();
611956
+ graph.close();
611957
+ }
611958
+ if (existingTelegramEpisodes < history.length) {
611959
+ for (const entry of history) {
611960
+ this.upsertTelegramReflectionHistoryEntry(sessionKey, entry);
611961
+ }
611962
+ }
611963
+ } catch {
611964
+ }
611965
+ }
611276
611966
  ensureAllTelegramConversationsLoaded() {
611277
611967
  if (this.loadedAllConversationState) return;
611278
611968
  this.loadedAllConversationState = true;
@@ -611304,6 +611994,7 @@ ${mediaContext}` : ""
611304
611994
  history: this.chatHistory.get(sessionKey) ?? [],
611305
611995
  participants,
611306
611996
  memoryCards: this.chatMemoryCards.get(sessionKey) ?? [],
611997
+ associativeMemory: this.telegramAssociativeMemoryForSession(sessionKey),
611307
611998
  stimulation: this.stimulation.getState(sessionKey),
611308
611999
  reflection: this.channelReflectionState.get(sessionKey) ?? { autoFollowup: false }
611309
612000
  };
@@ -611331,7 +612022,8 @@ ${mediaContext}` : ""
611331
612022
  };
611332
612023
  }
611333
612024
  buildTelegramChannelDaydreamInput(sessionKey, nowMs = Date.now()) {
611334
- const history = this.chatHistory.get(sessionKey) ?? [];
612025
+ const sqliteHistory = this.telegramSqliteHistoryForSession(sessionKey, 1500);
612026
+ const history = sqliteHistory.length > 0 ? sqliteHistory : this.chatHistory.get(sessionKey) ?? [];
611335
612027
  if (history.length === 0) return null;
611336
612028
  const last2 = [...history].reverse().find((entry) => entry.chatId !== void 0);
611337
612029
  if (!last2) return null;
@@ -611636,6 +612328,7 @@ ${mediaContext}` : ""
611636
612328
  this.upsertTelegramReflectionHistoryEntry(sessionKey, entry);
611637
612329
  this.updateTelegramParticipantProfile(sessionKey, msg, text);
611638
612330
  this.updateTelegramMemoryCards(sessionKey, entry);
612331
+ this.updateTelegramAssociativeMemory(sessionKey, entry);
611639
612332
  try {
611640
612333
  updateScopedPersonality(this.telegramPersonalityScope(sessionKey, msg), {
611641
612334
  speaker: telegramSpeakerLabel(msg),
@@ -611669,9 +612362,16 @@ ${mediaContext}` : ""
611669
612362
  chatTitle: msg.chatTitle
611670
612363
  };
611671
612364
  this.recordChatHistory(sessionKey, entry);
612365
+ this.persistTelegramAssistantMessage(
612366
+ msg,
612367
+ clean5,
612368
+ options2.messageId ?? void 0,
612369
+ options2.replyToMessageId
612370
+ );
611672
612371
  this.upsertTelegramReflectionHistoryEntry(sessionKey, entry);
611673
612372
  this.stimulation.recordAgentOutput(sessionKey);
611674
612373
  this.updateTelegramMemoryCards(sessionKey, entry);
612374
+ this.updateTelegramAssociativeMemory(sessionKey, entry);
611675
612375
  try {
611676
612376
  updateScopedPersonality(this.telegramPersonalityScope(sessionKey, msg), {
611677
612377
  speaker: entry.speaker || "Assistant",
@@ -611847,14 +612547,14 @@ ${mediaContext}` : ""
611847
612547
  profile.fromUserId = msg.fromUserId || profile.fromUserId;
611848
612548
  profile.messageCount += 1;
611849
612549
  profile.lastSeenTs = Date.now();
611850
- profile.lastMessage = truncateTelegramContextLine(text, 220);
612550
+ profile.lastMessage = stripTelegramHiddenThinking(text).replace(/\s+/g, " ").trim();
611851
612551
  if (msg.replyToMessageId) profile.replyCount += 1;
611852
612552
  if (this.state.botUsername && text.toLowerCase().includes(`@${this.state.botUsername.toLowerCase()}`)) {
611853
612553
  profile.directAddressCount += 1;
611854
612554
  }
611855
612555
  for (const tag of inferTelegramToneTags(text)) profile.toneTags.add(tag);
611856
612556
  if (text.trim()) {
611857
- profile.samples.push(truncateTelegramContextLine(text, 160));
612557
+ profile.samples.push(stripTelegramHiddenThinking(text).replace(/\s+/g, " ").trim());
611858
612558
  if (profile.samples.length > TELEGRAM_CONTEXT_SAMPLE_LIMIT) {
611859
612559
  profile.samples.splice(0, profile.samples.length - TELEGRAM_CONTEXT_SAMPLE_LIMIT);
611860
612560
  }
@@ -611862,8 +612562,162 @@ ${mediaContext}` : ""
611862
612562
  participants.set(participantKey, profile);
611863
612563
  this.chatParticipants.set(sessionKey, participants);
611864
612564
  }
612565
+ updateTelegramAssociativeMemory(sessionKey, entry) {
612566
+ const memory = this.telegramAssociativeMemoryForSession(sessionKey);
612567
+ const now = entry.ts ?? Date.now();
612568
+ memory.updatedAt = now;
612569
+ const speaker = telegramHistorySpeaker(entry);
612570
+ const actionId = createHash23("sha1").update(`${sessionKey}:${entry.role}:${entry.messageId ?? ""}:${now}:${entry.text}`).digest("hex").slice(0, 16);
612571
+ if (!memory.actions.some((action) => action.id === actionId)) {
612572
+ memory.actions.push({
612573
+ id: actionId,
612574
+ ts: now,
612575
+ role: entry.role,
612576
+ speaker,
612577
+ mode: entry.mode,
612578
+ text: stripTelegramHiddenThinking(entry.text || "").replace(/\s+/g, " ").trim(),
612579
+ messageId: entry.messageId,
612580
+ replyToMessageId: entry.replyToMessageId,
612581
+ userId: entry.fromUserId,
612582
+ username: entry.username
612583
+ });
612584
+ if (memory.actions.length > TELEGRAM_ASSOCIATIVE_ACTION_LIMIT) {
612585
+ memory.actions.splice(
612586
+ 0,
612587
+ memory.actions.length - TELEGRAM_ASSOCIATIVE_ACTION_LIMIT
612588
+ );
612589
+ }
612590
+ }
612591
+ if (entry.role === "user") {
612592
+ const userKey = String(entry.fromUserId || entry.username || entry.firstName || speaker);
612593
+ const existing = memory.users[userKey];
612594
+ const userMemory = existing ?? {
612595
+ userId: entry.fromUserId,
612596
+ username: entry.username || "unknown",
612597
+ displayName: entry.firstName || entry.username || speaker,
612598
+ aliases: [],
612599
+ firstSeenTs: now,
612600
+ lastSeenTs: now,
612601
+ messageCount: 0,
612602
+ directAddressCount: 0,
612603
+ replyCount: 0,
612604
+ toneTags: [],
612605
+ facts: [],
612606
+ relationshipHints: [],
612607
+ recentTopics: [],
612608
+ lastMessages: []
612609
+ };
612610
+ userMemory.userId = entry.fromUserId ?? userMemory.userId;
612611
+ userMemory.username = entry.username || userMemory.username;
612612
+ userMemory.displayName = entry.firstName || userMemory.displayName;
612613
+ for (const alias of [entry.username, entry.firstName, speaker].filter(Boolean)) {
612614
+ const clean5 = alias.replace(/^@/, "").trim();
612615
+ if (clean5 && !userMemory.aliases.includes(clean5)) userMemory.aliases.push(clean5);
612616
+ }
612617
+ userMemory.aliases = userMemory.aliases.slice(0, 20);
612618
+ userMemory.lastSeenTs = now;
612619
+ userMemory.messageCount += 1;
612620
+ if (entry.replyToMessageId) userMemory.replyCount += 1;
612621
+ if (this.state.botUsername && entry.text.toLowerCase().includes(`@${this.state.botUsername.toLowerCase()}`)) {
612622
+ userMemory.directAddressCount += 1;
612623
+ }
612624
+ for (const tag of inferTelegramToneTags(entry.text)) {
612625
+ if (!userMemory.toneTags.includes(tag)) userMemory.toneTags.push(tag);
612626
+ }
612627
+ userMemory.toneTags = userMemory.toneTags.slice(0, 20);
612628
+ const compact2 = stripTelegramHiddenThinking(entry.text || "").replace(/\s+/g, " ").trim();
612629
+ if (compact2) {
612630
+ userMemory.lastMessages.push(compact2);
612631
+ userMemory.lastMessages = userMemory.lastMessages.slice(-40);
612632
+ }
612633
+ for (const topic of telegramMemoryTags(entry.text, entry.mediaSummary).slice(0, 6)) {
612634
+ if (!userMemory.recentTopics.includes(topic)) userMemory.recentTopics.push(topic);
612635
+ }
612636
+ userMemory.recentTopics = userMemory.recentTopics.slice(-80);
612637
+ const facts = this.extractTelegramAssociativeFacts(entry, speaker);
612638
+ for (const text of facts) {
612639
+ const fact = this.upsertTelegramAssociativeFact(memory.facts, text, entry, speaker, 1.5);
612640
+ this.upsertTelegramAssociativeFact(userMemory.facts, fact.text, entry, speaker, fact.weight);
612641
+ }
612642
+ if (entry.replyContext?.sender || entry.replyToMessageId) {
612643
+ const target = entry.replyContext?.sender ? telegramReplySenderLabel(entry.replyContext.sender) : `message ${entry.replyToMessageId}`;
612644
+ const hint = `${speaker} replied to ${target}: ${stripTelegramHiddenThinking(entry.text || "").replace(/\s+/g, " ").trim()}`;
612645
+ if (!userMemory.relationshipHints.includes(hint)) {
612646
+ userMemory.relationshipHints.push(hint);
612647
+ userMemory.relationshipHints = userMemory.relationshipHints.slice(-80);
612648
+ }
612649
+ this.upsertTelegramAssociativeFact(memory.relationships, hint, entry, speaker, 1.2);
612650
+ }
612651
+ userMemory.facts.sort((a2, b) => b.weight - a2.weight || b.updatedAt - a2.updatedAt);
612652
+ userMemory.facts = userMemory.facts.slice(0, TELEGRAM_ASSOCIATIVE_USER_FACT_LIMIT);
612653
+ memory.users[userKey] = userMemory;
612654
+ }
612655
+ memory.facts.sort((a2, b) => b.weight - a2.weight || b.updatedAt - a2.updatedAt);
612656
+ memory.facts = memory.facts.slice(0, TELEGRAM_ASSOCIATIVE_FACT_LIMIT);
612657
+ memory.relationships.sort((a2, b) => b.updatedAt - a2.updatedAt);
612658
+ memory.relationships = memory.relationships.slice(0, TELEGRAM_ASSOCIATIVE_RELATION_LIMIT);
612659
+ }
612660
+ extractTelegramAssociativeFacts(entry, speaker) {
612661
+ if (entry.role !== "user") return [];
612662
+ const text = stripTelegramHiddenThinking(entry.text || "").replace(/\s+/g, " ").trim();
612663
+ const facts = /* @__PURE__ */ new Set();
612664
+ const patterns = [
612665
+ /\b(?:remember|note|keep in mind|for future reference)\s+(?:that\s+)?(.{4,260})/i,
612666
+ /\bmy name is\s+(.{2,120})/i,
612667
+ /\bcall me\s+(.{2,120})/i,
612668
+ /\bi\s+(?:am|work as|live in|like|love|prefer|use|run|have|need|want)\b.{0,220}/i,
612669
+ /\b(?:we|this group|our)\s+.{0,220}\b(?:is|are|uses|prefers|likes|works|has|needs)\b.{0,220}/i
612670
+ ];
612671
+ for (const pattern of patterns) {
612672
+ const match = text.match(pattern);
612673
+ if (!match) continue;
612674
+ const body = (match[1] || match[0]).trim().replace(/[.!?]*$/, "");
612675
+ if (body.length >= 3) facts.add(`${speaker}: ${body}`);
612676
+ }
612677
+ if (entry.mediaSummary) {
612678
+ facts.add(`${speaker} shared media: ${entry.mediaSummary}`);
612679
+ }
612680
+ return [...facts].slice(0, 8);
612681
+ }
612682
+ upsertTelegramAssociativeFact(facts, text, entry, speaker, weight = 1) {
612683
+ const clean5 = stripTelegramHiddenThinking(text || "").replace(/\s+/g, " ").trim();
612684
+ const key = clean5.toLowerCase();
612685
+ const now = entry.ts ?? Date.now();
612686
+ let fact = facts.find((item) => item.text.toLowerCase() === key);
612687
+ if (!fact) {
612688
+ fact = {
612689
+ id: createHash23("sha1").update(`${entry.chatId ?? ""}:${key}`).digest("hex").slice(0, 12),
612690
+ text: clean5,
612691
+ tags: telegramMemoryTags(clean5, entry.mediaSummary),
612692
+ speakers: [],
612693
+ userIds: [],
612694
+ usernames: [],
612695
+ messageIds: [],
612696
+ createdAt: now,
612697
+ updatedAt: now,
612698
+ weight
612699
+ };
612700
+ facts.push(fact);
612701
+ }
612702
+ fact.updatedAt = now;
612703
+ fact.weight = Math.min(10, Math.max(fact.weight, weight) + 0.2);
612704
+ if (!fact.speakers.includes(speaker)) fact.speakers.push(speaker);
612705
+ fact.speakers = fact.speakers.slice(0, 16);
612706
+ if (entry.fromUserId && !fact.userIds.includes(entry.fromUserId)) fact.userIds.push(entry.fromUserId);
612707
+ fact.userIds = fact.userIds.slice(0, 32);
612708
+ const username = (entry.username || "").replace(/^@/, "").toLowerCase();
612709
+ if (username && !fact.usernames.includes(username)) fact.usernames.push(username);
612710
+ fact.usernames = fact.usernames.slice(0, 32);
612711
+ if (entry.messageId && !fact.messageIds.includes(entry.messageId)) fact.messageIds.push(entry.messageId);
612712
+ fact.messageIds = fact.messageIds.slice(-80);
612713
+ for (const tag of telegramMemoryTags(clean5, entry.mediaSummary)) {
612714
+ if (!fact.tags.includes(tag)) fact.tags.push(tag);
612715
+ }
612716
+ fact.tags = fact.tags.slice(0, 16);
612717
+ return fact;
612718
+ }
611865
612719
  updateTelegramMemoryCards(sessionKey, entry) {
611866
- const text = truncateTelegramContextLine(entry.text, 500);
612720
+ const text = stripTelegramHiddenThinking(entry.text || "").replace(/\s+/g, " ").trim();
611867
612721
  if (!text || text.length < 3) return;
611868
612722
  const speaker = telegramHistorySpeaker(entry);
611869
612723
  const tags = telegramMemoryTags(text, entry.mediaSummary);
@@ -611955,6 +612809,304 @@ ${mediaContext}` : ""
611955
612809
  )
611956
612810
  })).sort((a2, b) => b.score - a2.score || b.card.updatedAt - a2.card.updatedAt).slice(0, limit);
611957
612811
  }
612812
+ relevantTelegramAssociativeMemoryContext(sessionKey, msg, limit = 12) {
612813
+ const memory = this.chatAssociativeMemory.get(sessionKey);
612814
+ if (!memory) return "";
612815
+ const queryTokens = telegramMemoryTokens([
612816
+ telegramSpeakerLabel(msg),
612817
+ msg.username || "",
612818
+ msg.firstName || "",
612819
+ msg.text,
612820
+ summarizeTelegramMessageAttachments(msg)
612821
+ ].join(" "));
612822
+ const currentUsername = (msg.username || "").replace(/^@/, "").toLowerCase();
612823
+ const userEntries = Object.entries(memory.users).filter(
612824
+ ([, user]) => msg.fromUserId !== void 0 && user.userId === msg.fromUserId || !!currentUsername && user.username.replace(/^@/, "").toLowerCase() === currentUsername || user.aliases.some((alias) => alias.replace(/^@/, "").toLowerCase() === currentUsername)
612825
+ );
612826
+ const scoredFacts = memory.facts.map((fact) => ({
612827
+ fact,
612828
+ score: telegramMemorySimilarity(
612829
+ queryTokens,
612830
+ telegramMemoryTokens([fact.text, fact.tags.join(" "), fact.speakers.join(" ")].join(" "))
612831
+ ) + (fact.userIds.includes(msg.fromUserId ?? -1) ? 0.35 : 0) + (currentUsername && fact.usernames.includes(currentUsername) ? 0.35 : 0)
612832
+ })).filter((item) => item.score > 0).sort((a2, b) => b.score - a2.score || b.fact.updatedAt - a2.fact.updatedAt).slice(0, limit);
612833
+ const relationshipFacts = memory.relationships.filter(
612834
+ (fact) => fact.userIds.includes(msg.fromUserId ?? -1) || !!currentUsername && fact.usernames.includes(currentUsername)
612835
+ ).slice(0, 6);
612836
+ const recentActions = memory.actions.filter(
612837
+ (action) => action.userId === msg.fromUserId || !!currentUsername && action.username?.replace(/^@/, "").toLowerCase() === currentUsername || action.role === "assistant"
612838
+ ).slice(-8);
612839
+ const sections = [];
612840
+ if (userEntries.length > 0) {
612841
+ const lines = userEntries.map(([, user]) => {
612842
+ const facts = user.facts.slice(0, 6).map((fact) => ` - ${telegramContextJsonString(fact.text, 220)}`).join("\n");
612843
+ const hints = user.relationshipHints.slice(-4).map((hint) => ` - relation=${telegramContextJsonString(hint, 200)}`).join("\n");
612844
+ const topics = user.recentTopics.slice(-8).join(", ") || "none";
612845
+ return [
612846
+ `- ${user.username && user.username !== "unknown" ? `@${user.username}` : user.displayName || "user"}${user.userId ? ` id:${user.userId}` : ""}: messages:${user.messageCount}, direct:${user.directAddressCount}, replies:${user.replyCount}, topics:${topics}`,
612847
+ facts,
612848
+ hints
612849
+ ].filter(Boolean).join("\n");
612850
+ });
612851
+ sections.push(`### Durable Associative User Memory
612852
+ ${lines.join("\n")}`);
612853
+ }
612854
+ if (scoredFacts.length > 0) {
612855
+ const lines = scoredFacts.map(
612856
+ ({ fact, score }) => `- (${score.toFixed(2)}) ${telegramContextJsonString(fact.text, 260)}${fact.messageIds.length ? ` [messages:${fact.messageIds.slice(-4).join(",")}]` : ""}`
612857
+ );
612858
+ sections.push(`### Durable Associative Memory Recall
612859
+ ${lines.join("\n")}`);
612860
+ }
612861
+ if (relationshipFacts.length > 0) {
612862
+ const lines = relationshipFacts.map((fact) => `- ${telegramContextJsonString(fact.text, 240)}`);
612863
+ sections.push(`### Durable Relationship Recall
612864
+ ${lines.join("\n")}`);
612865
+ }
612866
+ if (recentActions.length > 0) {
612867
+ const lines = recentActions.map(
612868
+ (action) => `- ${telegramHistoryTime({ ts: action.ts, role: action.role, text: action.text })} ${action.speaker}/${action.role}${action.mode ? `/${action.mode}` : ""}: ${telegramContextJsonString(action.text, 220)}`
612869
+ );
612870
+ sections.push(`### Durable Recent Action Ledger Recall
612871
+ ${lines.join("\n")}`);
612872
+ }
612873
+ return sections.join("\n\n");
612874
+ }
612875
+ relevantTelegramSqliteMirrorContext(sessionKey, msg, limit = 12) {
612876
+ const db = this.telegramDb();
612877
+ if (!db) return "";
612878
+ const rows = /* @__PURE__ */ new Map();
612879
+ const addRows = (items) => {
612880
+ for (const row of items) {
612881
+ const key = Number(row.rowid);
612882
+ if (Number.isFinite(key)) rows.set(key, row);
612883
+ }
612884
+ };
612885
+ try {
612886
+ if (msg.fromUserId !== void 0) {
612887
+ addRows(db.prepare(`
612888
+ SELECT rowid, * FROM telegram_messages
612889
+ WHERE session_key = ? AND from_user_id = ?
612890
+ ORDER BY received_at DESC
612891
+ LIMIT ?
612892
+ `).all(sessionKey, msg.fromUserId, limit));
612893
+ }
612894
+ const username = (msg.username || "").replace(/^@/, "").toLowerCase();
612895
+ if (username) {
612896
+ addRows(db.prepare(`
612897
+ SELECT rowid, * FROM telegram_messages
612898
+ WHERE session_key = ? AND lower(username) = ?
612899
+ ORDER BY received_at DESC
612900
+ LIMIT ?
612901
+ `).all(sessionKey, username, Math.max(4, Math.floor(limit / 2))));
612902
+ }
612903
+ const queryTokens = [...telegramMemoryTokens(msg.text)].slice(0, 6);
612904
+ for (const token of queryTokens) {
612905
+ addRows(db.prepare(`
612906
+ SELECT rowid, * FROM telegram_messages
612907
+ WHERE session_key = ? AND text LIKE ?
612908
+ ORDER BY received_at DESC
612909
+ LIMIT 4
612910
+ `).all(sessionKey, `%${token}%`));
612911
+ }
612912
+ } catch {
612913
+ return "";
612914
+ }
612915
+ const selected = [...rows.values()].sort((a2, b) => Number(b.received_at ?? 0) - Number(a2.received_at ?? 0)).slice(0, limit);
612916
+ if (selected.length === 0) return "";
612917
+ const lines = selected.map((row) => {
612918
+ const when = row.received_at ? new Date(Number(row.received_at)).toISOString() : "";
612919
+ const speaker = row.role === "assistant" ? `@${this.state.botUsername || "omnius"}` : row.username ? `@${row.username}` : row.from_user_id ? `user:${row.from_user_id}` : "unknown";
612920
+ const reply = row.reply_to_message_id ? ` reply_to:${row.reply_to_message_id}` : "";
612921
+ return `- ${when} ${speaker}/${row.role || "user"} msg:${row.message_id}${reply}: ${telegramContextJsonString(String(row.text || ""), 260)}`;
612922
+ });
612923
+ return [
612924
+ "### SQLite Telegram Raw Mirror Recall",
612925
+ "Durable local mirror rows for this scoped chat. Use as historical evidence when the rolling context omitted older turns.",
612926
+ lines.join("\n")
612927
+ ].join("\n");
612928
+ }
612929
+ /**
612930
+ * Always-on episodic memory recall for the active Telegram session.
612931
+ * Pulls scored episodes from .omnius/episodes.db where session_id = sessionKey,
612932
+ * including social / tool_result / reflection modalities. This surfaces day-old
612933
+ * context the rolling 36-turn window has already shed.
612934
+ */
612935
+ relevantTelegramEpisodicMemoryContext(sessionKey, msg, limit = 8) {
612936
+ if (!this.repoRoot) return "";
612937
+ const paths = omniusMemoryDbPaths(this.repoRoot);
612938
+ if (!existsSync112(paths.episodes)) return "";
612939
+ let episodes = [];
612940
+ const graph = new TemporalGraph(paths.knowledge);
612941
+ const store2 = new EpisodeStore(paths.episodes, graph);
612942
+ try {
612943
+ const recent = store2.search({ sessionId: sessionKey, limit: Math.max(limit * 3, 24) }) ?? [];
612944
+ const queryText = (msg.text || "").toLowerCase().trim();
612945
+ if (queryText.length > 3) {
612946
+ const qTokens = queryText.split(/\s+/).filter((t2) => t2.length >= 4);
612947
+ const scored = recent.map((ep) => {
612948
+ const content = String(ep.content || "").toLowerCase();
612949
+ const hits = qTokens.filter((t2) => content.includes(t2)).length;
612950
+ return { ep, score: hits };
612951
+ }).sort((a2, b) => b.score - a2.score || b.ep.timestamp - a2.ep.timestamp);
612952
+ episodes = scored.slice(0, limit).map((s2) => s2.ep);
612953
+ } else {
612954
+ episodes = recent.slice(0, limit);
612955
+ }
612956
+ } catch {
612957
+ return "";
612958
+ } finally {
612959
+ try {
612960
+ store2.close();
612961
+ } catch {
612962
+ }
612963
+ try {
612964
+ graph.close();
612965
+ } catch {
612966
+ }
612967
+ }
612968
+ if (episodes.length === 0) return "";
612969
+ const lines = episodes.map((ep) => {
612970
+ const when = ep.timestamp ? new Date(ep.timestamp).toISOString() : "";
612971
+ const meta = ep.metadata || {};
612972
+ const tg = meta.telegram && typeof meta.telegram === "object" ? meta.telegram : {};
612973
+ const speaker = tg.speaker || tg.username || (ep.modality === "tool_result" ? `tool:${ep.toolName || "?"}` : "agent");
612974
+ const mode = tg.mode ? `/${tg.mode}` : "";
612975
+ const text = ep.gist || ep.content;
612976
+ return `- ${when} ${speaker}${mode} [${ep.modality}]: ${telegramContextJsonString(String(text || ""), 260)}`;
612977
+ });
612978
+ return [
612979
+ "### Episodic Memory Recall (durable, day+ scope)",
612980
+ "Scored episodes for this Telegram session from episodes.db. These are PERSISTENT across restarts and survive the rolling-context window. Treat as canonical for older facts.",
612981
+ lines.join("\n")
612982
+ ].join("\n");
612983
+ }
612984
+ telegramSqliteHistoryForSession(sessionKey, limit = 1e3) {
612985
+ const db = this.telegramDb();
612986
+ if (!db) return [];
612987
+ try {
612988
+ const rows = db.prepare(`
612989
+ SELECT * FROM telegram_messages
612990
+ WHERE session_key = ?
612991
+ ORDER BY received_at DESC
612992
+ LIMIT ?
612993
+ `).all(sessionKey, limit);
612994
+ return rows.reverse().map((row) => ({
612995
+ role: row.role === "assistant" ? "assistant" : "user",
612996
+ text: String(row.text || ""),
612997
+ ts: Number(row.received_at || 0) || void 0,
612998
+ chatId: row.chat_id,
612999
+ speaker: row.role === "assistant" ? `@${this.state.botUsername || "omnius"}` : row.username ? `@${row.username}` : row.from_user_id ? `user:${row.from_user_id}` : "unknown",
613000
+ username: row.username || void 0,
613001
+ firstName: row.first_name || void 0,
613002
+ fromUserId: typeof row.from_user_id === "number" ? row.from_user_id : void 0,
613003
+ messageId: typeof row.message_id === "number" ? row.message_id : void 0,
613004
+ messageThreadId: typeof row.message_thread_id === "number" ? row.message_thread_id : void 0,
613005
+ replyToMessageId: typeof row.reply_to_message_id === "number" ? row.reply_to_message_id : void 0,
613006
+ chatType: row.chat_type,
613007
+ chatTitle: row.chat_title || void 0,
613008
+ mediaSummary: row.media_json ? "media attached in raw Telegram SQLite mirror" : void 0
613009
+ }));
613010
+ } catch {
613011
+ return [];
613012
+ }
613013
+ }
613014
+ searchTelegramSqliteMirrorRows(sessionKey, query, options2 = {}) {
613015
+ const db = this.telegramDb();
613016
+ if (!db) return [];
613017
+ const limit = Math.max(1, Math.min(50, Math.floor(options2.limit ?? 12)));
613018
+ const username = (options2.username || "").replace(/^@/, "").trim().toLowerCase();
613019
+ const tokens = [...telegramMemoryTokens(query)].slice(0, 8);
613020
+ const rows = /* @__PURE__ */ new Map();
613021
+ const addRows = (items) => {
613022
+ for (const row of items) {
613023
+ const key = Number(row.rowid);
613024
+ if (Number.isFinite(key)) rows.set(key, row);
613025
+ }
613026
+ };
613027
+ try {
613028
+ if (options2.userId !== void 0) {
613029
+ addRows(db.prepare(`
613030
+ SELECT rowid, * FROM telegram_messages
613031
+ WHERE session_key = ? AND from_user_id = ?
613032
+ ORDER BY received_at DESC
613033
+ LIMIT ?
613034
+ `).all(sessionKey, options2.userId, limit));
613035
+ }
613036
+ if (username) {
613037
+ addRows(db.prepare(`
613038
+ SELECT rowid, * FROM telegram_messages
613039
+ WHERE session_key = ? AND lower(username) = ?
613040
+ ORDER BY received_at DESC
613041
+ LIMIT ?
613042
+ `).all(sessionKey, username, limit));
613043
+ }
613044
+ if (tokens.length > 0) {
613045
+ const clauses = tokens.map(() => "text LIKE ?").join(" OR ");
613046
+ addRows(db.prepare(`
613047
+ SELECT rowid, * FROM telegram_messages
613048
+ WHERE session_key = ? AND (${clauses})
613049
+ ORDER BY received_at DESC
613050
+ LIMIT ?
613051
+ `).all(sessionKey, ...tokens.map((token) => `%${token}%`), limit));
613052
+ }
613053
+ if (rows.size === 0) {
613054
+ addRows(db.prepare(`
613055
+ SELECT rowid, * FROM telegram_messages
613056
+ WHERE session_key = ?
613057
+ ORDER BY received_at DESC
613058
+ LIMIT ?
613059
+ `).all(sessionKey, Math.min(limit, 12)));
613060
+ }
613061
+ } catch {
613062
+ return [];
613063
+ }
613064
+ return [...rows.values()].sort((a2, b) => Number(b.received_at ?? 0) - Number(a2.received_at ?? 0)).slice(0, limit);
613065
+ }
613066
+ formatTelegramSqliteMirrorRows(rows, maxText = 260) {
613067
+ return rows.map((row) => {
613068
+ const when = row.received_at ? new Date(Number(row.received_at)).toISOString() : "";
613069
+ const speaker = row.role === "assistant" ? `@${this.state.botUsername || "omnius"}` : row.username ? `@${row.username}` : row.from_user_id ? `user:${row.from_user_id}` : "unknown";
613070
+ const reply = row.reply_to_message_id ? ` reply_to:${row.reply_to_message_id}` : "";
613071
+ const media = row.media_json ? " media:attached" : "";
613072
+ return `- ${when} ${speaker}/${row.role || "user"} msg:${row.message_id}${reply}${media}: ${telegramContextJsonString(String(row.text || ""), maxText)}`;
613073
+ }).join("\n");
613074
+ }
613075
+ searchTelegramEpisodeMemory(sessionKey, query, limit = 8) {
613076
+ if (!this.repoRoot || !query.trim()) return [];
613077
+ const paths = omniusMemoryDbPaths(this.repoRoot);
613078
+ if (!existsSync112(paths.episodes)) return [];
613079
+ const graph = new TemporalGraph(paths.knowledge);
613080
+ const store2 = new EpisodeStore(paths.episodes, graph);
613081
+ try {
613082
+ return store2.searchWithPPR(
613083
+ {
613084
+ sessionId: sessionKey,
613085
+ query,
613086
+ limit: Math.max(1, Math.min(20, Math.floor(limit))),
613087
+ metadataFilter: { sourceSurface: "telegram" }
613088
+ },
613089
+ { lexicalWeight: 1.25, embeddingWeight: 0 }
613090
+ );
613091
+ } catch {
613092
+ return [];
613093
+ } finally {
613094
+ store2.close();
613095
+ graph.close();
613096
+ }
613097
+ }
613098
+ formatTelegramEpisodeMemoryResults(episodes, maxText = 320) {
613099
+ return episodes.map((episode) => {
613100
+ const meta = episode.metadata;
613101
+ const telegram = meta?.telegram && typeof meta.telegram === "object" ? meta.telegram : {};
613102
+ const when = episode.timestamp ? new Date(episode.timestamp).toISOString() : "";
613103
+ const speaker = telegram.speaker || telegram.username || "unknown";
613104
+ const messageId = telegram.messageId ? ` msg:${telegram.messageId}` : "";
613105
+ const mode = telegram.mode ? `/${telegram.mode}` : "";
613106
+ const text = episode.content.split("\n").filter((line) => !/^(Telegram|session:|chat:|message_id:|thread_id:|speaker:|mode:)/i.test(line.trim())).join(" ").replace(/\s+/g, " ").trim() || episode.content;
613107
+ return `- ${when} ${speaker}${mode}${messageId}: ${telegramContextJsonString(text, maxText)}`;
613108
+ }).join("\n");
613109
+ }
611958
613110
  buildTelegramConversationContextStream(sessionKey, msg, maxRecent = TELEGRAM_CONTEXT_RECENT_DEFAULT) {
611959
613111
  this.ensureTelegramConversationLoaded(sessionKey);
611960
613112
  const history = this.chatHistory.get(sessionKey) ?? [];
@@ -611992,6 +613144,33 @@ ${mediaContext}` : ""
611992
613144
  });
611993
613145
  sections.push(`### Participants And Relationship Signals
611994
613146
  ${participantLines.join("\n")}`);
613147
+ }
613148
+ const associativeContext = this.relevantTelegramAssociativeMemoryContext(
613149
+ sessionKey,
613150
+ msg,
613151
+ isGroup ? 14 : 8
613152
+ );
613153
+ if (associativeContext) {
613154
+ sections.push(associativeContext);
613155
+ }
613156
+ const sqliteMirrorContext = this.relevantTelegramSqliteMirrorContext(
613157
+ sessionKey,
613158
+ msg,
613159
+ isGroup ? 14 : 8
613160
+ );
613161
+ if (sqliteMirrorContext) {
613162
+ sections.push(sqliteMirrorContext);
613163
+ }
613164
+ try {
613165
+ const episodicContext = this.relevantTelegramEpisodicMemoryContext(
613166
+ sessionKey,
613167
+ msg,
613168
+ isGroup ? 10 : 6
613169
+ );
613170
+ if (episodicContext) {
613171
+ sections.push(episodicContext);
613172
+ }
613173
+ } catch {
611995
613174
  }
611996
613175
  const memoryCards = this.relevantTelegramMemoryCards(sessionKey, msg, isGroup ? 10 : 6);
611997
613176
  if (memoryCards.length > 0) {
@@ -612176,7 +613355,8 @@ ${lines.join("\n")}`);
612176
613355
  ``,
612177
613356
  `Reply discretion: infer from the live thread, speaker relationships, direct platform signals, replies, tone, current message, and any private channel daydream artifact supplied in context. Do not use static keyword rules.`,
612178
613357
  `Private chats: should_reply is normally true.`,
612179
- `Group/public chats: default should_reply to false unless the current message clearly addresses the bot, replies to the bot, continues an active bot-involved exchange, assigns the bot work, or asks for the bot's view. Ambient chatter, third-person discussion about the bot, commands meant for a human, or questions among other people are false. Do not set true just because the bot could help.`,
613358
+ `Group/public chats: default should_reply to false unless the current message clearly addresses the bot, replies to the bot, continues an active bot-involved exchange, assigns the bot work, asks for the bot's view, or is semantically connected to durable memory/current discussion in a way where a concise bot reply is socially useful. Ambient chatter, third-person discussion about the bot, commands meant for a human, or questions among other people are false. Do not set true just because the bot could help.`,
613359
+ `Memory discipline: use durable associative user memory, relationships, prior actions, and recent context to infer whether this speaker is continuing a bot-related thread. A mention is not required when the semantic target is clearly the bot or an ongoing bot-mediated discussion.`,
612180
613360
  `Channel daydream discipline: a daydream artifact may highlight relationship signals, unresolved questions, or possible reply opportunities from idle reflection. It can justify analyzing this turn, but it does not force a reply. Reply only if the current user entry makes the intervention timely and socially appropriate.`,
612181
613361
  `Stimulation discipline: also set attention_state, attention_delta, and optional next_check_after_messages/next_check_after_ms. These control future analysis cadence only; they do not force a reply. Use engaged for active back-and-forth, observing for likely relevant context, cooldown for recently irrelevant context, and idle for ambient chatter.`,
612182
613362
  forcedLine,
@@ -612558,6 +613738,40 @@ ${TELEGRAM_PUBLIC_ORCHESTRATOR_CONTRACT}`);
612558
613738
  if (!me?.ok) {
612559
613739
  throw new Error(`Invalid Telegram bot token: ${me?.description || "unknown error"}`);
612560
613740
  }
613741
+ if (this.repoRoot && me.result?.id) {
613742
+ try {
613743
+ const lockDir = resolve43(this.repoRoot, ".omnius", "telegram-runner-state");
613744
+ mkdirSync65(lockDir, { recursive: true });
613745
+ const lockFile = join127(lockDir, `bot-${me.result.id}.owner.lock`);
613746
+ if (existsSync112(lockFile)) {
613747
+ try {
613748
+ const prior = JSON.parse(readFileSync92(lockFile, "utf8"));
613749
+ const priorAlive = typeof prior.pid === "number" && prior.pid !== process.pid ? this.processIsAlive(prior.pid) : false;
613750
+ if (priorAlive) {
613751
+ throw new Error(
613752
+ `Telegram bot @${prior.botUsername || me.result.username || "unknown"} is already being polled by pid ${prior.pid} (cwd ${prior.cwd || "?"}). Stop that instance or use a different bot token in this project.`
613753
+ );
613754
+ }
613755
+ } catch (e2) {
613756
+ if (e2 instanceof Error && e2.message.startsWith("Telegram bot @")) throw e2;
613757
+ }
613758
+ }
613759
+ writeFileSync59(
613760
+ lockFile,
613761
+ JSON.stringify({
613762
+ pid: process.pid,
613763
+ cwd: this.repoRoot,
613764
+ botUsername: me.result.username,
613765
+ botUserId: me.result.id,
613766
+ ts: Date.now()
613767
+ }, null, 2),
613768
+ { encoding: "utf-8", mode: 384 }
613769
+ );
613770
+ this.telegramOwnerLockFile = lockFile;
613771
+ } catch (e2) {
613772
+ if (e2 instanceof Error && e2.message.startsWith("Telegram bot @")) throw e2;
613773
+ }
613774
+ }
612561
613775
  this.state = {
612562
613776
  active: true,
612563
613777
  botUsername: me.result?.username ?? "unknown",
@@ -612601,10 +613815,40 @@ ${TELEGRAM_PUBLIC_ORCHESTRATOR_CONTRACT}`);
612601
613815
  agent.aborted = true;
612602
613816
  if (agent.typingInterval) clearInterval(agent.typingInterval);
612603
613817
  }
613818
+ if (this.telegramSqliteDb && this.telegramSqliteDb !== false) {
613819
+ try {
613820
+ this.telegramSqliteDb.close();
613821
+ } catch {
613822
+ }
613823
+ this.telegramSqliteDb = null;
613824
+ }
613825
+ if (this.telegramOwnerLockFile) {
613826
+ try {
613827
+ if (existsSync112(this.telegramOwnerLockFile)) {
613828
+ unlinkSync22(this.telegramOwnerLockFile);
613829
+ }
613830
+ } catch {
613831
+ }
613832
+ this.telegramOwnerLockFile = null;
613833
+ }
612604
613834
  this.subAgents.clear();
612605
613835
  this.activeChatViews.clear();
612606
613836
  this.refreshActiveTelegramInteractionCount();
612607
613837
  }
613838
+ /**
613839
+ * Cheap liveness probe: kill(pid, 0) throws ESRCH when the process is dead
613840
+ * and EPERM when it exists but we can't signal it (still alive from our POV).
613841
+ */
613842
+ processIsAlive(pid) {
613843
+ if (!pid || !Number.isFinite(pid)) return false;
613844
+ try {
613845
+ process.kill(pid, 0);
613846
+ return true;
613847
+ } catch (e2) {
613848
+ if (e2 && e2.code === "EPERM") return true;
613849
+ return false;
613850
+ }
613851
+ }
612608
613852
  // ── Typing indicator ──────────────────────────────────────────────────
612609
613853
  /** Start sending "typing" indicator every 4 seconds */
612610
613854
  startTypingIndicator(chatId) {
@@ -613529,7 +614773,7 @@ ${currentTelegramPrompt}`;
613529
614773
  const toolHint = [
613530
614774
  "You have access to isolated per-chat memory (memory_write, memory_read, memory_search) scoped to this conversation.",
613531
614775
  "memory_search may use scope=group/current_chat for this group or scope=user with user_id/username for a participant in this same group. Other groups, admin chats, and private DMs are not accessible here.",
613532
- "You can remember facts about users and retrieve them later. You also have web_search and web_fetch to look up information.",
614776
+ "You can remember facts about users and retrieve them later. Durable associative memory in the prompt includes participant profiles, relationships, scoped facts, and prior actions retained across days, sessions, and Omnius updates. You also have web_search and web_fetch to look up information.",
613533
614777
  "You have the full scoped Telegram media-analysis stack by default: telegram_media_recent, image_read, ocr, ocr_image_advanced, vision, pdf_to_text, ocr_pdf, transcribe_file, video_understand, audio_analyze, and identity_memory. For complex textual imagery, screenshots, forms, scans, or dense labels, prefer ocr_image_advanced after resolving media with path='reply' or path='latest'.",
613534
614778
  formatIdentityMemoryContext(chatLabel || "Telegram private chat"),
613535
614779
  reminderToolContract,
@@ -613695,7 +614939,7 @@ ${result.llmContent ?? result.output}` };
613695
614939
  if (tool.name === "memory_search") {
613696
614940
  return {
613697
614941
  ...tool,
613698
- description: "Search only this Telegram chat's isolated Zettelkasten memory cards. Supports scope=group/current_chat or scope=user with user_id/username, but never crosses into admin/private/global memory.",
614942
+ description: "Search only this Telegram chat's isolated durable memory: raw SQLite message mirror, episode/knowledge graph recall, associative user facts, and memory cards. Supports scope=group/current_chat or scope=user with user_id/username, but never crosses into admin/private/global memory.",
613699
614943
  parameters: (() => {
613700
614944
  const base3 = tool.parameters ?? {};
613701
614945
  const props = base3["properties"] && typeof base3["properties"] === "object" && !Array.isArray(base3["properties"]) ? base3["properties"] : {};
@@ -613713,6 +614957,8 @@ ${result.llmContent ?? result.output}` };
613713
614957
  execute: async (args) => {
613714
614958
  const query = String(args["query"] || "").trim();
613715
614959
  const maxResults = typeof args["max_results"] === "number" && Number.isFinite(args["max_results"]) ? Math.max(1, Math.min(20, Math.floor(args["max_results"]))) : 8;
614960
+ if (!query) return { success: true, output: "Search query is required." };
614961
+ this.ensureTelegramConversationLoaded(msgSessionKey);
613716
614962
  const currentGroupId = chatId === void 0 ? "" : String(chatId);
613717
614963
  const requestedGroupId = String(args["group_id"] ?? args["chat_id"] ?? "").trim();
613718
614964
  if (requestedGroupId && currentGroupId && requestedGroupId !== currentGroupId) {
@@ -613740,32 +614986,70 @@ ${result.llmContent ?? result.output}` };
613740
614986
  if (effectiveUsername && haystack.includes(`@${effectiveUsername}`)) return true;
613741
614987
  return false;
613742
614988
  });
613743
- if (!query || cards.length === 0) {
613744
- const scopeLabel2 = wantsUserScope ? `user ${effectiveUsername ? `@${effectiveUsername}` : `id ${effectiveUserId}`}` : `chat ${currentGroupId || msgSessionKey}`;
613745
- return { success: true, output: cards.length === 0 ? `No scoped memories found for ${scopeLabel2}.` : "Search query is required." };
613746
- }
613747
614989
  const queryTokens = telegramMemoryTokens(query);
613748
- const results = cards.map((card) => ({
614990
+ const cardResults = cards.map((card) => ({
613749
614991
  card,
613750
614992
  score: telegramMemorySimilarity(
613751
614993
  queryTokens,
613752
614994
  telegramMemoryTokens([card.title, card.tags.join(" "), card.speakers.join(" "), card.notes.join(" ")].join(" "))
613753
614995
  )
613754
614996
  })).filter((item) => item.score > 0).sort((a2, b) => b.score - a2.score || b.card.updatedAt - a2.card.updatedAt).slice(0, maxResults);
613755
- if (results.length === 0) {
613756
- return { success: true, output: `No scoped memories matched "${query}" in this Telegram chat.` };
613757
- }
613758
- const lines = results.map(({ card, score }) => {
614997
+ const cardLines = cardResults.map(({ card, score }) => {
613759
614998
  const notes2 = card.notes.slice(-4).map((note) => ` - ${truncateTelegramContextLine(note, 220)}`).join("\n");
613760
614999
  const tags = card.tags.length ? ` tags:${card.tags.slice(0, 8).join(",")}` : "";
613761
615000
  const users = Array.isArray(card.userIds) && card.userIds.length ? ` users:${card.userIds.slice(0, 6).join(",")}` : "";
613762
615001
  return `[${card.id}] ${card.title} (relevance ${score.toFixed(2)}${users}${tags})
613763
615002
  ${notes2}`;
613764
615003
  });
615004
+ const memory = this.chatAssociativeMemory.get(msgSessionKey);
615005
+ const associativeFacts = memory ? [...memory.facts, ...memory.relationships].filter((fact) => {
615006
+ if (!wantsUserScope) return true;
615007
+ if (effectiveUserId !== void 0 && fact.userIds.includes(effectiveUserId)) return true;
615008
+ return !!effectiveUsername && fact.usernames.includes(effectiveUsername);
615009
+ }).map((fact) => ({
615010
+ fact,
615011
+ score: telegramMemorySimilarity(
615012
+ queryTokens,
615013
+ telegramMemoryTokens([fact.text, fact.tags.join(" "), fact.speakers.join(" ")].join(" "))
615014
+ ) + (effectiveUserId !== void 0 && fact.userIds.includes(effectiveUserId) ? 0.3 : 0) + (effectiveUsername && fact.usernames.includes(effectiveUsername) ? 0.3 : 0)
615015
+ })).filter((item) => item.score > 0).sort((a2, b) => b.score - a2.score || b.fact.updatedAt - a2.fact.updatedAt).slice(0, maxResults) : [];
615016
+ const rawRows = this.searchTelegramSqliteMirrorRows(msgSessionKey, query, {
615017
+ limit: maxResults,
615018
+ userId: effectiveUserId,
615019
+ username: effectiveUsername
615020
+ });
615021
+ const episodeResults = this.searchTelegramEpisodeMemory(msgSessionKey, query, maxResults);
613765
615022
  const scopeLabel = wantsUserScope ? `user ${effectiveUsername ? `@${effectiveUsername}` : `id ${effectiveUserId}`}` : `chat ${currentGroupId || msgSessionKey}`;
613766
- return { success: true, output: `Scoped Telegram memory search for "${query}" in ${scopeLabel}:
613767
-
613768
- ${lines.join("\n\n")}` };
615023
+ const sections = [];
615024
+ if (cardLines.length > 0) {
615025
+ sections.push(`### Scoped Memory Cards
615026
+ ${cardLines.join("\n\n")}`);
615027
+ }
615028
+ if (associativeFacts.length > 0) {
615029
+ const lines = associativeFacts.map(
615030
+ ({ fact, score }) => `- (${score.toFixed(2)}) ${telegramContextJsonString(fact.text, 300)}${fact.messageIds.length ? ` [messages:${fact.messageIds.slice(-6).join(",")}]` : ""}`
615031
+ );
615032
+ sections.push(`### Durable Associative Facts
615033
+ ${lines.join("\n")}`);
615034
+ }
615035
+ if (episodeResults.length > 0) {
615036
+ sections.push(`### Episode/Knowledge Graph Recall
615037
+ ${this.formatTelegramEpisodeMemoryResults(episodeResults)}`);
615038
+ }
615039
+ if (rawRows.length > 0) {
615040
+ sections.push(`### Raw SQLite Message Mirror
615041
+ ${this.formatTelegramSqliteMirrorRows(rawRows)}`);
615042
+ }
615043
+ if (sections.length === 0) {
615044
+ return { success: true, output: `No scoped Telegram memories matched "${query}" in ${scopeLabel}.` };
615045
+ }
615046
+ const output = [
615047
+ `Scoped Telegram memory search for "${query}" in ${scopeLabel}.`,
615048
+ "Results are scoped to this Telegram chat and may include raw message evidence, graph episodes, associative facts, and memory cards.",
615049
+ "",
615050
+ sections.join("\n\n")
615051
+ ].join("\n");
615052
+ return { success: true, output, llmContent: output };
613769
615053
  }
613770
615054
  };
613771
615055
  }
@@ -616320,6 +617604,7 @@ ${caption}\r
616320
617604
  }
616321
617605
  const msg = normalizeTelegramUpdate(update2);
616322
617606
  if (!msg) continue;
617607
+ this.persistTelegramRawMessage(update2, msg);
616323
617608
  const isAdmin = this.adminUserId ? String(msg.fromUserId) === this.adminUserId || msg.username === this.adminUserId : false;
616324
617609
  if (this.adminUserId && !this.agentConfig) {
616325
617610
  if (!isAdmin) continue;
@@ -616665,8 +617950,8 @@ function appendCheckin(sessionId, steering) {
616665
617950
  mkdirSync66(sessionsDir(), { recursive: true });
616666
617951
  const fp = checkinPath(sessionId);
616667
617952
  const entry = JSON.stringify({ ts: Date.now(), steering }) + "\n";
616668
- const { appendFileSync: appendFileSync9 } = __require("node:fs");
616669
- appendFileSync9(fp, entry, "utf-8");
617953
+ const { appendFileSync: appendFileSync12 } = __require("node:fs");
617954
+ appendFileSync12(fp, entry, "utf-8");
616670
617955
  } catch {
616671
617956
  }
616672
617957
  }
@@ -618865,7 +620150,7 @@ __export(audit_log_exports, {
618865
620150
  recordAudit: () => recordAudit,
618866
620151
  sanitizeBody: () => sanitizeBody
618867
620152
  });
618868
- import { mkdirSync as mkdirSync69, appendFileSync as appendFileSync7, readFileSync as readFileSync96, existsSync as existsSync116 } from "node:fs";
620153
+ import { mkdirSync as mkdirSync69, appendFileSync as appendFileSync10, readFileSync as readFileSync96, existsSync as existsSync116 } from "node:fs";
618869
620154
  import { join as join131 } from "node:path";
618870
620155
  function initAuditLog(omniusDir) {
618871
620156
  auditDir = join131(omniusDir, "audit");
@@ -618880,7 +620165,7 @@ function recordAudit(record) {
618880
620165
  if (!initialized) return;
618881
620166
  try {
618882
620167
  const line = JSON.stringify(record) + "\n";
618883
- appendFileSync7(auditFile, line, "utf-8");
620168
+ appendFileSync10(auditFile, line, "utf-8");
618884
620169
  } catch {
618885
620170
  }
618886
620171
  }
@@ -641179,7 +642464,7 @@ import { fileURLToPath as fileURLToPath18 } from "node:url";
641179
642464
  import {
641180
642465
  readFileSync as readFileSync105,
641181
642466
  writeFileSync as writeFileSync70,
641182
- appendFileSync as appendFileSync8,
642467
+ appendFileSync as appendFileSync11,
641183
642468
  rmSync as rmSync7,
641184
642469
  readdirSync as readdirSync46,
641185
642470
  mkdirSync as mkdirSync78
@@ -645673,7 +646958,7 @@ This is an independent background session started from /background.`
645673
646958
  if (!line.trim()) return;
645674
646959
  try {
645675
646960
  mkdirSync78(HISTORY_DIR, { recursive: true });
645676
- appendFileSync8(HISTORY_FILE, line + "\n", "utf8");
646961
+ appendFileSync11(HISTORY_FILE, line + "\n", "utf8");
645677
646962
  if (Math.random() < 0.02) {
645678
646963
  const all2 = readFileSync105(HISTORY_FILE, "utf8").trim().split("\n");
645679
646964
  if (all2.length > MAX_HISTORY_LINES) {
@@ -647127,7 +648412,9 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
647127
648412
  ...settings.admin !== void 0 ? { telegramAdmin: settings.admin } : {},
647128
648413
  ...settings.mode !== void 0 ? { telegramMode: settings.mode } : {}
647129
648414
  };
647130
- if (settings.local) {
648415
+ const projectHasOmnius = existsSync127(join143(repoRoot, ".omnius"));
648416
+ const useProject = settings.local === true || settings.local === void 0 && projectHasOmnius;
648417
+ if (useProject) {
647131
648418
  saveProjectSettings(repoRoot, payload);
647132
648419
  } else {
647133
648420
  saveGlobalSettings(payload);
@@ -651460,12 +652747,12 @@ function crashLog(label, err) {
651460
652747
  const logLine = `[${timestamp}] ${label}: ${msg}
651461
652748
  `;
651462
652749
  try {
651463
- const { appendFileSync: appendFileSync9, mkdirSync: mkdirSync81 } = __require("node:fs");
652750
+ const { appendFileSync: appendFileSync12, mkdirSync: mkdirSync81 } = __require("node:fs");
651464
652751
  const { join: join148 } = __require("node:path");
651465
652752
  const { homedir: homedir52 } = __require("node:os");
651466
652753
  const logDir = join148(homedir52(), ".omnius");
651467
652754
  mkdirSync81(logDir, { recursive: true });
651468
- appendFileSync9(join148(logDir, "crash.log"), logLine);
652755
+ appendFileSync12(join148(logDir, "crash.log"), logLine);
651469
652756
  } catch {
651470
652757
  }
651471
652758
  try {