omnius 1.0.59 → 1.0.60

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
@@ -561982,8 +561982,7 @@ function wrapTaskCompleteSummary(summary) {
561982
561982
  }
561983
561983
  function wrapPlainLine(line, width) {
561984
561984
  const out = [];
561985
- const indent = line.match(/^\s*/)?.[0] ?? "";
561986
- const continuationIndent = indent.length > 0 ? indent : "";
561985
+ const continuationIndent = hangingIndentForPlainLine(line, width);
561987
561986
  let remaining = line;
561988
561987
  while (remaining.length > width) {
561989
561988
  let breakAt = remaining.lastIndexOf(" ", width);
@@ -561994,6 +561993,20 @@ function wrapPlainLine(line, width) {
561994
561993
  out.push(remaining);
561995
561994
  return out;
561996
561995
  }
561996
+ function hangingIndentForPlainLine(line, width) {
561997
+ const capped = (n2) => " ".repeat(Math.max(0, Math.min(n2, width - 4)));
561998
+ const patterns = [
561999
+ /^(\s*(?:[-*+○•]\s+))/,
562000
+ /^(\s*(?:\d+[.)]\s+))/,
562001
+ /^(\s*(?:>\s*))/,
562002
+ /^(\s*(?:[A-Za-z][\w.-]{0,28}:\s+))/
562003
+ ];
562004
+ for (const pattern of patterns) {
562005
+ const match = line.match(pattern);
562006
+ if (match?.[1]) return capped(match[1].length);
562007
+ }
562008
+ return capped(line.match(/^\s*/)?.[0].length ?? 0);
562009
+ }
561997
562010
  function renderTaskIncomplete(turns, toolCalls, durationMs, tokens) {
561998
562011
  const duration = formatDuration2(durationMs);
561999
562012
  const tokenStr = tokens ? ` ${formatTokenCount(tokens)}` : "";
@@ -563699,7 +563712,7 @@ var init_voice_session = __esm({
563699
563712
 
563700
563713
  // packages/cli/src/tui/scoped-personality.ts
563701
563714
  import { createHash as createHash17 } from "node:crypto";
563702
- import { existsSync as existsSync82, mkdirSync as mkdirSync46, readFileSync as readFileSync65, writeFileSync as writeFileSync41 } from "node:fs";
563715
+ import { appendFileSync as appendFileSync4, existsSync as existsSync82, mkdirSync as mkdirSync46, readFileSync as readFileSync65, writeFileSync as writeFileSync41 } from "node:fs";
563703
563716
  import { join as join98, resolve as resolve37 } from "node:path";
563704
563717
  function safeName(input) {
563705
563718
  return input.replace(/[^A-Za-z0-9_.-]/g, "-").slice(0, 80) || "default";
@@ -563715,7 +563728,8 @@ function scopedPersonalityPaths(scope) {
563715
563728
  const prefix = `${safeName(scope.label)}-${scopeHash(scope)}`;
563716
563729
  return {
563717
563730
  json: join98(dir, `${prefix}.json`),
563718
- markdown: join98(dir, `${prefix}.md`)
563731
+ markdown: join98(dir, `${prefix}.md`),
563732
+ events: join98(dir, `${prefix}.events.jsonl`)
563719
563733
  };
563720
563734
  }
563721
563735
  function inferToneTags(text) {
@@ -563776,16 +563790,49 @@ function newDocument(scope) {
563776
563790
  "Do not expose private local files, secrets, credentials, or hidden reasoning in public or group scopes.",
563777
563791
  "Do not claim memory is unavailable when scoped context is present."
563778
563792
  ],
563793
+ topicCounts: {},
563794
+ durableObservations: [],
563795
+ relationshipEvents: [],
563779
563796
  recentObservations: []
563780
563797
  };
563781
563798
  }
563799
+ function normalizeDocument(scope, parsed) {
563800
+ const fresh = newDocument(scope);
563801
+ const doc = {
563802
+ ...fresh,
563803
+ ...parsed,
563804
+ scope: {
563805
+ kind: scope.kind,
563806
+ idHash: scopeHash(scope),
563807
+ label: scope.label || parsed.scope?.label || fresh.scope.label
563808
+ },
563809
+ participants: parsed.participants && typeof parsed.participants === "object" ? parsed.participants : {},
563810
+ dominantTone: Array.isArray(parsed.dominantTone) ? parsed.dominantTone : [],
563811
+ responseStyle: Array.isArray(parsed.responseStyle) && parsed.responseStyle.length > 0 ? parsed.responseStyle : fresh.responseStyle,
563812
+ relationshipModel: Array.isArray(parsed.relationshipModel) && parsed.relationshipModel.length > 0 ? parsed.relationshipModel : fresh.relationshipModel,
563813
+ behavioralGuidance: Array.isArray(parsed.behavioralGuidance) && parsed.behavioralGuidance.length > 0 ? parsed.behavioralGuidance : fresh.behavioralGuidance,
563814
+ boundaries: Array.isArray(parsed.boundaries) && parsed.boundaries.length > 0 ? parsed.boundaries : fresh.boundaries,
563815
+ topicCounts: parsed.topicCounts && typeof parsed.topicCounts === "object" ? parsed.topicCounts : {},
563816
+ durableObservations: Array.isArray(parsed.durableObservations) ? parsed.durableObservations.slice(-MAX_PROFILE_DURABLE_OBSERVATIONS) : [],
563817
+ relationshipEvents: Array.isArray(parsed.relationshipEvents) ? parsed.relationshipEvents.slice(-MAX_PROFILE_RELATIONSHIP_EVENTS) : [],
563818
+ recentObservations: Array.isArray(parsed.recentObservations) ? parsed.recentObservations.slice(-MAX_PROFILE_OBSERVATIONS) : []
563819
+ };
563820
+ doc.messageCount = Number.isFinite(doc.messageCount) ? doc.messageCount : 0;
563821
+ for (const profile of Object.values(doc.participants)) {
563822
+ if (!Array.isArray(profile.toneTags)) profile.toneTags = [];
563823
+ if (!Array.isArray(profile.samples)) profile.samples = [];
563824
+ profile.toneTags = profile.toneTags.slice(0, MAX_PROFILE_TAGS);
563825
+ profile.samples = profile.samples.slice(-MAX_PROFILE_SAMPLES);
563826
+ }
563827
+ return doc;
563828
+ }
563782
563829
  function loadScopedPersonality(scope) {
563783
563830
  const paths = scopedPersonalityPaths(scope);
563784
563831
  if (!existsSync82(paths.json)) return newDocument(scope);
563785
563832
  try {
563786
563833
  const parsed = JSON.parse(readFileSync65(paths.json, "utf8"));
563787
563834
  if (parsed.version !== PROFILE_VERSION) return newDocument(scope);
563788
- return parsed;
563835
+ return normalizeDocument(scope, parsed);
563789
563836
  } catch {
563790
563837
  return newDocument(scope);
563791
563838
  }
@@ -563796,6 +563843,14 @@ function saveScopedPersonality(scope, doc) {
563796
563843
  writeFileSync41(paths.json, JSON.stringify(doc, null, 2) + "\n", "utf8");
563797
563844
  writeFileSync41(paths.markdown, renderScopedPersonalityMarkdown(doc) + "\n", "utf8");
563798
563845
  }
563846
+ function appendScopedPersonalityEvent(scope, event) {
563847
+ try {
563848
+ const paths = scopedPersonalityPaths(scope);
563849
+ mkdirSync46(scopedPersonalityDir(scope.repoRoot, scope.kind), { recursive: true });
563850
+ appendFileSync4(paths.events, JSON.stringify(event) + "\n", "utf8");
563851
+ } catch {
563852
+ }
563853
+ }
563799
563854
  function updateScopedPersonality(scope, observation) {
563800
563855
  const doc = loadScopedPersonality(scope);
563801
563856
  const now = new Date(observation.ts ?? Date.now()).toISOString();
@@ -563806,6 +563861,7 @@ function updateScopedPersonality(scope, observation) {
563806
563861
  ...inferToneTags(observation.text),
563807
563862
  ...keywords(observation.text)
563808
563863
  ];
563864
+ const topicTags = keywords(observation.text);
563809
563865
  doc.updatedAt = now;
563810
563866
  doc.messageCount += 1;
563811
563867
  doc.scope.label = scope.label;
@@ -563831,6 +563887,9 @@ function updateScopedPersonality(scope, observation) {
563831
563887
  for (const tag of profile.toneTags) allTags.set(tag, (allTags.get(tag) ?? 0) + profile.count);
563832
563888
  }
563833
563889
  doc.dominantTone = [...allTags.entries()].sort((a2, b) => b[1] - a2[1]).map(([tag]) => tag).slice(0, MAX_PROFILE_TAGS);
563890
+ for (const tag of topicTags) {
563891
+ doc.topicCounts[tag] = (doc.topicCounts[tag] ?? 0) + 1;
563892
+ }
563834
563893
  if (doc.dominantTone.includes("frustrated")) {
563835
563894
  doc.responseStyle = [
563836
563895
  "Start with the concrete fix or answer; avoid soothing filler.",
@@ -563852,15 +563911,31 @@ function updateScopedPersonality(scope, observation) {
563852
563911
  for (const hint of relationshipHints) {
563853
563912
  const clean5 = compactLine(hint, 180);
563854
563913
  if (clean5 && !doc.relationshipModel.includes(clean5)) doc.relationshipModel.push(clean5);
563914
+ if (clean5 && !doc.relationshipEvents.includes(`${now} ${clean5}`)) doc.relationshipEvents.push(`${now} ${clean5}`);
563855
563915
  }
563856
563916
  doc.relationshipModel = doc.relationshipModel.slice(-12);
563917
+ doc.relationshipEvents = doc.relationshipEvents.slice(-MAX_PROFILE_RELATIONSHIP_EVENTS);
563857
563918
  if (text) {
563858
563919
  const line = `${now} ${speaker}${observation.mode ? `/${observation.mode}` : ""}: ${text}`;
563859
563920
  doc.recentObservations.push(line);
563860
563921
  if (doc.recentObservations.length > MAX_PROFILE_OBSERVATIONS) {
563861
563922
  doc.recentObservations.splice(0, doc.recentObservations.length - MAX_PROFILE_OBSERVATIONS);
563862
563923
  }
563924
+ doc.durableObservations.push(line);
563925
+ if (doc.durableObservations.length > MAX_PROFILE_DURABLE_OBSERVATIONS) {
563926
+ doc.durableObservations.splice(0, doc.durableObservations.length - MAX_PROFILE_DURABLE_OBSERVATIONS);
563927
+ }
563863
563928
  }
563929
+ appendScopedPersonalityEvent(scope, {
563930
+ ts: observation.ts ?? Date.now(),
563931
+ iso: now,
563932
+ speaker,
563933
+ role: observation.role,
563934
+ mode: observation.mode,
563935
+ text,
563936
+ toneTags: [...new Set(toneTags)].slice(0, 32),
563937
+ relationshipHints
563938
+ });
563864
563939
  saveScopedPersonality(scope, doc);
563865
563940
  return doc;
563866
563941
  }
@@ -563871,6 +563946,9 @@ function renderScopedPersonalityMarkdown(doc) {
563871
563946
  return `- ${name10}: messages=${profile.count}; last=${profile.lastSeenAt};${tags}${samples ? `
563872
563947
  ${samples}` : ""}`;
563873
563948
  });
563949
+ const topTopics = Object.entries(doc.topicCounts ?? {}).sort((a2, b) => b[1] - a2[1]).slice(0, 18).map(([topic, count]) => `- ${topic}: ${count}`);
563950
+ const durable = doc.durableObservations.slice(-18).map((line) => `- ${line}`);
563951
+ const relationships = doc.relationshipEvents.slice(-10).map((line) => `- ${line}`);
563874
563952
  return [
563875
563953
  `# Scoped Personality Profile`,
563876
563954
  ``,
@@ -563887,6 +563965,11 @@ ${samples}` : ""}`;
563887
563965
  `## Relationship Model`,
563888
563966
  doc.relationshipModel.map((line) => `- ${line}`).join("\n"),
563889
563967
  ``,
563968
+ `## Durable Profile Memory`,
563969
+ topTopics.length ? [`Top recurring topics:`, ...topTopics].join("\n") : "Top recurring topics:\n- none",
563970
+ durable.length ? [`Longer-term observations:`, ...durable].join("\n") : "Longer-term observations:\n- none",
563971
+ relationships.length ? [`Relationship events:`, ...relationships].join("\n") : "Relationship events:\n- none",
563972
+ ``,
563890
563973
  `## Behavioral Guidance`,
563891
563974
  doc.behavioralGuidance.map((line) => `- ${line}`).join("\n"),
563892
563975
  ``,
@@ -563910,13 +563993,15 @@ function renderScopedPersonalityContext(doc) {
563910
563993
  function buildScopedPersonalityContext(scope) {
563911
563994
  return renderScopedPersonalityContext(loadScopedPersonality(scope));
563912
563995
  }
563913
- var PROFILE_VERSION, MAX_PROFILE_OBSERVATIONS, MAX_PROFILE_SAMPLES, MAX_PROFILE_TAGS, STOPWORDS2;
563996
+ var PROFILE_VERSION, MAX_PROFILE_OBSERVATIONS, MAX_PROFILE_DURABLE_OBSERVATIONS, MAX_PROFILE_RELATIONSHIP_EVENTS, MAX_PROFILE_SAMPLES, MAX_PROFILE_TAGS, STOPWORDS2;
563914
563997
  var init_scoped_personality = __esm({
563915
563998
  "packages/cli/src/tui/scoped-personality.ts"() {
563916
563999
  "use strict";
563917
564000
  PROFILE_VERSION = 1;
563918
- MAX_PROFILE_OBSERVATIONS = 18;
563919
- MAX_PROFILE_SAMPLES = 8;
564001
+ MAX_PROFILE_OBSERVATIONS = 40;
564002
+ MAX_PROFILE_DURABLE_OBSERVATIONS = 240;
564003
+ MAX_PROFILE_RELATIONSHIP_EVENTS = 120;
564004
+ MAX_PROFILE_SAMPLES = 16;
563920
564005
  MAX_PROFILE_TAGS = 12;
563921
564006
  STOPWORDS2 = /* @__PURE__ */ new Set([
563922
564007
  "about",
@@ -567727,7 +567812,7 @@ __export(omnius_directory_exports, {
567727
567812
  writeIndexMeta: () => writeIndexMeta,
567728
567813
  writeTaskHandoff: () => writeTaskHandoff2
567729
567814
  });
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";
567815
+ 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
567816
  import { join as join104, relative as relative9, basename as basename17, dirname as dirname29 } from "node:path";
567732
567817
  import { homedir as homedir31 } from "node:os";
567733
567818
  import { createHash as createHash20 } from "node:crypto";
@@ -568248,6 +568333,7 @@ function saveSessionContext(repoRoot, entry) {
568248
568333
  const contextDir = join104(repoRoot, OMNIUS_DIR, "context");
568249
568334
  mkdirSync50(contextDir, { recursive: true });
568250
568335
  const filePath = join104(contextDir, CONTEXT_SAVE_FILE);
568336
+ const ledgerPath = join104(contextDir, CONTEXT_LEDGER_FILE);
568251
568337
  const lockPath = join104(contextDir, CONTEXT_SAVE_FILE + ".lock");
568252
568338
  const locked = acquireLock(lockPath);
568253
568339
  if (!locked) {
@@ -568264,7 +568350,23 @@ function saveSessionContext(repoRoot, entry) {
568264
568350
  } catch {
568265
568351
  ctx3 = { entries: [], maxEntries: MAX_CONTEXT_ENTRIES, updatedAt: "" };
568266
568352
  }
568353
+ ctx3.maxEntries = Math.max(
568354
+ Number.isFinite(ctx3.maxEntries) ? ctx3.maxEntries : 0,
568355
+ MAX_CONTEXT_ENTRIES
568356
+ );
568267
568357
  const normalizedEntry = normalizeSessionContextEntry(entry);
568358
+ try {
568359
+ appendFileSync5(
568360
+ ledgerPath,
568361
+ JSON.stringify({
568362
+ ...normalizedEntry,
568363
+ ledgerSavedAt: (/* @__PURE__ */ new Date()).toISOString(),
568364
+ ledgerVersion: 1
568365
+ }) + "\n",
568366
+ "utf-8"
568367
+ );
568368
+ } catch {
568369
+ }
568268
568370
  const hashToIndex = /* @__PURE__ */ new Map();
568269
568371
  for (let i2 = 0; i2 < ctx3.entries.length; i2++) {
568270
568372
  const existing = ctx3.entries[i2];
@@ -568316,7 +568418,7 @@ function saveSessionContext(repoRoot, entry) {
568316
568418
  try {
568317
568419
  writeFileSync45(
568318
568420
  join104(contextDir, "session-diary.md"),
568319
- renderSessionDiary(ctx3.entries.slice(-10)),
568421
+ renderSessionDiary(ctx3.entries.slice(-MAX_SESSION_DIARY_ENTRIES)),
568320
568422
  "utf-8"
568321
568423
  );
568322
568424
  } catch {
@@ -568744,7 +568846,7 @@ function deleteUsageRecord(kind, value2, repoRoot) {
568744
568846
  remove(join104(repoRoot, OMNIUS_DIR, USAGE_HISTORY_FILE));
568745
568847
  }
568746
568848
  }
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;
568849
+ 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
568850
  var init_omnius_directory = __esm({
568749
568851
  "packages/cli/src/tui/omnius-directory.ts"() {
568750
568852
  "use strict";
@@ -568764,7 +568866,9 @@ var init_omnius_directory = __esm({
568764
568866
  PENDING_TASK_FILE = "pending-task.json";
568765
568867
  HANDOFF_FILE = "task-handoff.json";
568766
568868
  CONTEXT_SAVE_FILE = "session-context.json";
568767
- MAX_CONTEXT_ENTRIES = 20;
568869
+ CONTEXT_LEDGER_FILE = "session-context.events.jsonl";
568870
+ MAX_CONTEXT_ENTRIES = 200;
568871
+ MAX_SESSION_DIARY_ENTRIES = 80;
568768
568872
  SAME_TASK_REPLACE_WINDOW_MS = 12 * 60 * 60 * 1e3;
568769
568873
  LOCK_TIMEOUT_MS = 5e3;
568770
568874
  LOCK_RETRY_MS = 50;
@@ -572786,6 +572890,106 @@ ${CONTENT_BG_SEQ}`);
572786
572890
  (seq) => seq.endsWith("m") ? seq : ""
572787
572891
  );
572788
572892
  }
572893
+ reflowContentLines(livePartialLine, width) {
572894
+ const maxWidth = Math.max(16, width);
572895
+ const source = livePartialLine ? [...this._contentLines, livePartialLine] : this._contentLines;
572896
+ return source.flatMap(
572897
+ (line, idx) => this.reflowContentLine(line, maxWidth).map((segment) => ({
572898
+ line: segment,
572899
+ bufferIdx: idx
572900
+ }))
572901
+ );
572902
+ }
572903
+ reflowContentLine(line, width) {
572904
+ const visible = stripAnsi(line);
572905
+ if (visible.length <= width) return [line];
572906
+ const continuationIndent = this.hangingIndentForVisibleLine(
572907
+ visible,
572908
+ width
572909
+ );
572910
+ const ranges = this.visibleWrapRanges(
572911
+ visible,
572912
+ width,
572913
+ continuationIndent.length
572914
+ );
572915
+ if (ranges.length <= 1) return [line];
572916
+ return ranges.map((range, idx) => {
572917
+ const raw = this.rawSliceForVisibleRange(line, range.start, range.end);
572918
+ return idx === 0 ? raw : continuationIndent + raw.trimStart();
572919
+ });
572920
+ }
572921
+ hangingIndentForVisibleLine(visible, width) {
572922
+ const capped = (n2) => " ".repeat(Math.max(0, Math.min(n2, width - 4)));
572923
+ const patterns = [
572924
+ /^(\s*(?:[│├└]\s*)?(?:[-*+○•]\s+))/,
572925
+ /^(\s*(?:[│├└]\s*)?(?:\d+[.)]\s+))/,
572926
+ /^(\s*(?:[│├└]\s*)?(?:>\s*))/,
572927
+ /^(\s*(?:[│├└]\s*)?(?:[A-Za-z][\w.-]{0,28}:\s+))/
572928
+ ];
572929
+ for (const pattern of patterns) {
572930
+ const match = visible.match(pattern);
572931
+ if (match?.[1]) return capped(match[1].length);
572932
+ }
572933
+ return capped(visible.match(/^\s*/)?.[0].length ?? 0);
572934
+ }
572935
+ visibleWrapRanges(visible, width, continuationIndent) {
572936
+ const ranges = [];
572937
+ let start2 = 0;
572938
+ let available = width;
572939
+ while (start2 < visible.length) {
572940
+ if (visible.length - start2 <= available) {
572941
+ ranges.push({ start: start2, end: visible.length });
572942
+ break;
572943
+ }
572944
+ const limit = start2 + available;
572945
+ let breakAt = visible.lastIndexOf(" ", limit);
572946
+ if (breakAt <= start2 + 2) breakAt = limit;
572947
+ let end = breakAt;
572948
+ while (end > start2 && /\s/.test(visible[end - 1] ?? "")) end--;
572949
+ ranges.push({ start: start2, end: Math.max(start2 + 1, end) });
572950
+ start2 = breakAt;
572951
+ while (start2 < visible.length && /\s/.test(visible[start2] ?? "")) start2++;
572952
+ available = Math.max(8, width - continuationIndent);
572953
+ }
572954
+ return ranges;
572955
+ }
572956
+ rawSliceForVisibleRange(line, start2, end) {
572957
+ const startRaw = this.rawIndexForVisibleColumn(line, start2);
572958
+ const endRaw = this.rawIndexForVisibleColumn(line, end);
572959
+ const activeStyle = this.activeSgrAt(line, startRaw);
572960
+ const raw = line.slice(startRaw, endRaw);
572961
+ return activeStyle ? `${activeStyle}${raw}${RESET2}` : raw;
572962
+ }
572963
+ rawIndexForVisibleColumn(line, target) {
572964
+ if (target <= 0) return 0;
572965
+ let visible = 0;
572966
+ for (let i2 = 0; i2 < line.length; i2++) {
572967
+ if (line.charCodeAt(i2) === 27) {
572968
+ const match = line.slice(i2).match(/^\x1B\[[0-?]*[ -/]*[@-~]/);
572969
+ if (match) {
572970
+ i2 += match[0].length - 1;
572971
+ continue;
572972
+ }
572973
+ }
572974
+ if (visible >= target) return i2;
572975
+ visible++;
572976
+ }
572977
+ return line.length;
572978
+ }
572979
+ activeSgrAt(line, rawIndex) {
572980
+ let active = "";
572981
+ const sgr = /\x1B\[[0-9;]*m/g;
572982
+ let match;
572983
+ while ((match = sgr.exec(line)) && match.index < rawIndex) {
572984
+ const seq = match[0];
572985
+ if (seq === RESET2 || /\x1B\[(?:0|39|49)(?:;0)?m/.test(seq)) {
572986
+ active = "";
572987
+ } else {
572988
+ active += seq;
572989
+ }
572990
+ }
572991
+ return active;
572992
+ }
572789
572993
  /**
572790
572994
  * Remove the last N lines from the content scrollback buffer and repaint.
572791
572995
  * Used by Esc-to-recall to erase the just-rendered user prompt.
@@ -572876,9 +573080,10 @@ ${CONTENT_BG_SEQ}`);
572876
573080
  repaintContent() {
572877
573081
  const h = this.contentHeight;
572878
573082
  const livePartialLine = this.getLiveBufferedLine();
572879
- const totalLines = this._contentLines.length + (livePartialLine ? 1 : 0);
572880
- const startIdx = Math.max(0, totalLines - h - this._contentScrollOffset);
572881
573083
  const w = termCols();
573084
+ const reflowedLines = this.reflowContentLines(livePartialLine, w);
573085
+ const totalLines = reflowedLines.length;
573086
+ const startIdx = Math.max(0, totalLines - h - this._contentScrollOffset);
572882
573087
  const headerSafeFloor = layout().headerBottom + 1;
572883
573088
  let buf = "\x1B[?2026h";
572884
573089
  buf += "\x1B7";
@@ -572886,16 +573091,12 @@ ${CONTENT_BG_SEQ}`);
572886
573091
  const selRanges = this._textSelection.getSelectedRanges();
572887
573092
  for (let row = 0; row < h; row++) {
572888
573093
  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
- }
573094
+ const reflowed = reflowedLines[lineIdx];
573095
+ let line = reflowed?.line ?? "";
572895
573096
  const screenRow = this.scrollRegionTop + row;
572896
573097
  if (screenRow < headerSafeFloor) continue;
572897
573098
  line = line.replace(/\x1B\[0m/g, `\x1B[0m${CONTENT_BG_SEQ}`);
572898
- const sel = selRanges.find((r2) => r2.bufferIdx === lineIdx);
573099
+ const sel = selRanges.find((r2) => r2.bufferIdx === reflowed?.bufferIdx);
572899
573100
  if (sel) {
572900
573101
  line = TextSelection.applyHighlight(line, sel.startCol, sel.endCol);
572901
573102
  }
@@ -575667,7 +575868,7 @@ __export(setup_exports, {
575667
575868
  import * as readline from "node:readline";
575668
575869
  import { execSync as execSync50, spawn as spawn26, exec as exec4 } from "node:child_process";
575669
575870
  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";
575871
+ import { existsSync as existsSync90, writeFileSync as writeFileSync47, readFileSync as readFileSync74, appendFileSync as appendFileSync6, mkdirSync as mkdirSync52 } from "node:fs";
575671
575872
  import { join as join107 } from "node:path";
575672
575873
  import { homedir as homedir34, platform as platform5 } from "node:os";
575673
575874
  function wrapText(value2, width) {
@@ -578034,7 +578235,7 @@ function ensurePathInShellRc(binDir) {
578034
578235
  const exportLine = `
578035
578236
  export PATH="${binDir}:$PATH" # Added by omnius for nvim
578036
578237
  `;
578037
- appendFileSync4(rcFile, exportLine, "utf8");
578238
+ appendFileSync6(rcFile, exportLine, "utf8");
578038
578239
  console.log(` Added ${binDir} to ${rcFile}`);
578039
578240
  } catch {
578040
578241
  }
@@ -587604,7 +587805,7 @@ import {
587604
587805
  lstatSync,
587605
587806
  statSync as statSync37,
587606
587807
  rmSync as rmSync4,
587607
- appendFileSync as appendFileSync5
587808
+ appendFileSync as appendFileSync7
587608
587809
  } from "node:fs";
587609
587810
  import { relative as relative11, join as join114 } from "node:path";
587610
587811
  async function _immediateReregister(newUrl) {
@@ -592004,7 +592205,7 @@ sleep 1
592004
592205
  const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
592005
592206
  `;
592006
592207
  try {
592007
- appendFileSync5(_spLogFile, line);
592208
+ appendFileSync7(_spLogFile, line);
592008
592209
  } catch {
592009
592210
  }
592010
592211
  };
@@ -602934,10 +603135,13 @@ var init_stream_renderer = __esm({
602934
603135
  const words = text.split(/(\s+)/);
602935
603136
  const lines = [];
602936
603137
  let currentLine = "";
603138
+ const continuationIndent = this.hangingIndentForText(text, maxWidth);
603139
+ let available = maxWidth;
602937
603140
  for (const segment of words) {
602938
- if (currentLine.length + segment.length > maxWidth && currentLine.trim()) {
603141
+ if (currentLine.length + segment.length > available && currentLine.trim()) {
602939
603142
  lines.push(currentLine.trimEnd());
602940
- currentLine = segment.replace(/^\s+/, "");
603143
+ currentLine = continuationIndent + segment.replace(/^\s+/, "");
603144
+ available = maxWidth;
602941
603145
  } else {
602942
603146
  currentLine += segment;
602943
603147
  }
@@ -602947,12 +603151,26 @@ var init_stream_renderer = __esm({
602947
603151
  }
602948
603152
  return lines.length > 0 ? lines : [text];
602949
603153
  }
603154
+ hangingIndentForText(text, maxWidth) {
603155
+ const capped = (n2) => " ".repeat(Math.max(0, Math.min(n2, maxWidth - 4)));
603156
+ const patterns = [
603157
+ /^(\s*(?:[-*+○•]\s+))/,
603158
+ /^(\s*(?:\d+[.)]\s+))/,
603159
+ /^(\s*(?:>\s*))/,
603160
+ /^(\s*(?:[A-Za-z][\w.-]{0,28}:\s+))/
603161
+ ];
603162
+ for (const pattern of patterns) {
603163
+ const match = text.match(pattern);
603164
+ if (match?.[1]) return capped(match[1].length);
603165
+ }
603166
+ return capped(text.match(/^\s*/)?.[0].length ?? 0);
603167
+ }
602950
603168
  };
602951
603169
  }
602952
603170
  });
602953
603171
 
602954
603172
  // packages/cli/src/tui/edit-history.ts
602955
- import { appendFileSync as appendFileSync6, mkdirSync as mkdirSync60 } from "node:fs";
603173
+ import { appendFileSync as appendFileSync8, mkdirSync as mkdirSync60 } from "node:fs";
602956
603174
  import { join as join119 } from "node:path";
602957
603175
  function createEditHistoryLogger(repoRoot, sessionId) {
602958
603176
  const historyDir = join119(repoRoot, ".omnius", "history");
@@ -602973,7 +603191,7 @@ function createEditHistoryLogger(repoRoot, sessionId) {
602973
603191
  args: sanitizeArgs(toolName, toolArgs)
602974
603192
  };
602975
603193
  try {
602976
- appendFileSync6(logPath3, JSON.stringify(entry) + "\n", "utf-8");
603194
+ appendFileSync8(logPath3, JSON.stringify(entry) + "\n", "utf-8");
602977
603195
  } catch {
602978
603196
  }
602979
603197
  }
@@ -608356,6 +608574,9 @@ function upsertTelegramReflectionMessage(repoRoot, sessionKey, entry, options2)
608356
608574
  content,
608357
608575
  labels: [entry.mode, entry.mediaSummary, senderLabel(entry)].filter((value2) => Boolean(value2))
608358
608576
  });
608577
+ if (typeof entry.ts === "number" && Number.isFinite(entry.ts) && result.episodeId) {
608578
+ store2.getDb().prepare("UPDATE episodes SET timestamp = ? WHERE id = ?").run(entry.ts, result.episodeId);
608579
+ }
608359
608580
  addTextEdges(graph, result, entry);
608360
608581
  const after = store2.count(sessionKey);
608361
608582
  return { episodeId: result.episodeId, reused: after === before };
@@ -608418,6 +608639,9 @@ async function buildTelegramReflectionCorpus(options2) {
608418
608639
  content,
608419
608640
  labels: [entry.mode, entry.mediaSummary, senderLabel(entry)].filter((value2) => Boolean(value2))
608420
608641
  });
608642
+ if (typeof entry.ts === "number" && Number.isFinite(entry.ts) && result.episodeId) {
608643
+ store2.getDb().prepare("UPDATE episodes SET timestamp = ? WHERE id = ?").run(entry.ts, result.episodeId);
608644
+ }
608421
608645
  addTextEdges(graph, result, entry);
608422
608646
  const after = store2.count(options2.sessionKey);
608423
608647
  if (after === before) reusedEpisodes++;
@@ -609061,7 +609285,7 @@ var init_vision_ingress = __esm({
609061
609285
  });
609062
609286
 
609063
609287
  // 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";
609288
+ 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
609289
  import { join as join127, resolve as resolve43, basename as basename27, relative as relative13, isAbsolute as isAbsolute8, extname as extname16 } from "node:path";
609066
609290
  import { writeFile as writeFileAsync } from "node:fs/promises";
609067
609291
  import { createHash as createHash23, randomBytes as randomBytes22, randomInt } from "node:crypto";
@@ -610277,7 +610501,7 @@ function renderTelegramSubAgentError(username, error) {
610277
610501
  process.stdout.write(` ${c3.dim("│")} ${c3.red("✘")} @${username}: ${c3.dim(preview)}
610278
610502
  `);
610279
610503
  }
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;
610504
+ 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
610505
  var init_telegram_bridge = __esm({
610282
610506
  "packages/cli/src/tui/telegram-bridge.ts"() {
610283
610507
  "use strict";
@@ -610296,7 +610520,9 @@ var init_telegram_bridge = __esm({
610296
610520
  init_visual_identity_association();
610297
610521
  init_telegram_channel_dmn();
610298
610522
  init_telegram_reflection_corpus();
610523
+ init_memory_paths();
610299
610524
  init_telegram_reflection_extraction();
610525
+ init_dist7();
610300
610526
  TELEGRAM_TOOL_ACTION_GROUPS = [
610301
610527
  "read",
610302
610528
  "message",
@@ -610458,6 +610684,7 @@ PUBLIC TELEGRAM MEMORY SCOPE
610458
610684
 
610459
610685
  This turn may use memory and conversation history for the current Telegram group/private chat scope only.
610460
610686
  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.
610687
+ 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
610688
  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
610689
  `.trim();
610463
610690
  TELEGRAM_PUBLIC_VISION_STACK_CONTRACT = `
@@ -610538,12 +610765,16 @@ External acquisition contract:
610538
610765
  /^hmm,?\s+(let me|maybe|wait)\b/i,
610539
610766
  /^ok(ay)?,?\s+let me try\b/i
610540
610767
  ];
610541
- TELEGRAM_CHAT_HISTORY_LIMIT = 240;
610768
+ TELEGRAM_CHAT_HISTORY_LIMIT = 5e3;
610542
610769
  TELEGRAM_CONTEXT_RECENT_DEFAULT = 36;
610543
610770
  TELEGRAM_CONTEXT_LINE_LIMIT = 320;
610544
610771
  TELEGRAM_CONTEXT_SAMPLE_LIMIT = 10;
610545
- TELEGRAM_MEMORY_CARD_LIMIT = 80;
610546
- TELEGRAM_MEMORY_NOTE_LIMIT = 8;
610772
+ TELEGRAM_MEMORY_CARD_LIMIT = 240;
610773
+ TELEGRAM_MEMORY_NOTE_LIMIT = 24;
610774
+ TELEGRAM_ASSOCIATIVE_FACT_LIMIT = 600;
610775
+ TELEGRAM_ASSOCIATIVE_USER_FACT_LIMIT = 80;
610776
+ TELEGRAM_ASSOCIATIVE_ACTION_LIMIT = 5e3;
610777
+ TELEGRAM_ASSOCIATIVE_RELATION_LIMIT = 300;
610547
610778
  TELEGRAM_MEMORY_STOPWORDS = /* @__PURE__ */ new Set([
610548
610779
  "about",
610549
610780
  "after",
@@ -610627,6 +610858,7 @@ External acquisition contract:
610627
610858
  this.telegramToolPolicy = resolveSettings(repoRoot || ".").telegramToolPolicy ?? {};
610628
610859
  this.mediaCacheDir = resolve43(repoRoot || ".", ".omnius", "telegram-media-cache");
610629
610860
  this.telegramConversationDir = resolve43(repoRoot || ".", ".omnius", "telegram-conversations");
610861
+ this.telegramSqlitePath = resolve43(repoRoot || ".", ".omnius", "telegram.sqlite");
610630
610862
  this.telegramToolButtonDir = resolve43(repoRoot || ".", ".omnius", "telegram-tool-buttons");
610631
610863
  }
610632
610864
  botToken;
@@ -610658,6 +610890,8 @@ External acquisition contract:
610658
610890
  chatParticipants = /* @__PURE__ */ new Map();
610659
610891
  /** Lightweight Zettelkasten-style memory cards by chat/guest session key */
610660
610892
  chatMemoryCards = /* @__PURE__ */ new Map();
610893
+ /** Durable associative memory by scoped Telegram chat key. */
610894
+ chatAssociativeMemory = /* @__PURE__ */ new Map();
610661
610895
  /** Generic chronological attention cadence shared by live surfaces. */
610662
610896
  stimulation = new StimulationController();
610663
610897
  /** Throttles noisy "skipped group chatter" waterfall logs */
@@ -610707,6 +610941,9 @@ External acquisition contract:
610707
610941
  mediaCacheDir;
610708
610942
  /** Persistent conversation memory directory */
610709
610943
  telegramConversationDir;
610944
+ /** Durable SQLite mirror for raw Telegram messages and metadata. */
610945
+ telegramSqlitePath;
610946
+ telegramSqliteDb = null;
610710
610947
  /** Session keys loaded from persistent conversation memory */
610711
610948
  loadedConversationState = /* @__PURE__ */ new Set();
610712
610949
  /** True once persisted Telegram conversation scopes have been bulk-loaded. */
@@ -610993,12 +611230,26 @@ No scoped reflection artifact exists yet for this chat. Use <code>/reflect</code
610993
611230
  }
610994
611231
  recordChatHistory(sessionKey, entry) {
610995
611232
  this.ensureTelegramConversationLoaded(sessionKey);
611233
+ const stamped = { ...entry, ts: entry.ts ?? Date.now() };
610996
611234
  const existing = this.chatHistory.get(sessionKey) ?? [];
610997
- existing.push({ ...entry, ts: entry.ts ?? Date.now() });
611235
+ existing.push(stamped);
610998
611236
  if (existing.length > TELEGRAM_CHAT_HISTORY_LIMIT) {
610999
611237
  existing.splice(0, existing.length - TELEGRAM_CHAT_HISTORY_LIMIT);
611000
611238
  }
611001
611239
  this.chatHistory.set(sessionKey, existing);
611240
+ this.appendTelegramConversationLedger(sessionKey, stamped);
611241
+ }
611242
+ appendTelegramConversationLedger(sessionKey, entry) {
611243
+ if (!this.repoRoot) return;
611244
+ try {
611245
+ mkdirSync65(this.telegramConversationDir, { recursive: true });
611246
+ appendFileSync9(
611247
+ this.telegramConversationLedgerPath(sessionKey),
611248
+ JSON.stringify({ sessionKey, ...entry }) + "\n",
611249
+ "utf8"
611250
+ );
611251
+ } catch {
611252
+ }
611002
611253
  }
611003
611254
  telegramReplySenderWithSelfFlag(sender) {
611004
611255
  if (!sender) return void 0;
@@ -611226,6 +611477,272 @@ ${mediaContext}` : ""
611226
611477
  const safe = createHash23("sha1").update(sessionKey).digest("hex").slice(0, 20);
611227
611478
  return join127(this.telegramConversationDir, `${safe}.json`);
611228
611479
  }
611480
+ telegramConversationLedgerPath(sessionKey) {
611481
+ const safe = createHash23("sha1").update(sessionKey).digest("hex").slice(0, 20);
611482
+ return join127(this.telegramConversationDir, `${safe}.events.jsonl`);
611483
+ }
611484
+ telegramDb() {
611485
+ if (this.telegramSqliteDb === false) return null;
611486
+ if (this.telegramSqliteDb) return this.telegramSqliteDb;
611487
+ if (!this.repoRoot) {
611488
+ this.telegramSqliteDb = false;
611489
+ return null;
611490
+ }
611491
+ try {
611492
+ mkdirSync65(resolve43(this.repoRoot, ".omnius"), { recursive: true });
611493
+ const db = initDb(this.telegramSqlitePath);
611494
+ db.exec(`
611495
+ CREATE TABLE IF NOT EXISTS telegram_messages (
611496
+ session_key TEXT NOT NULL,
611497
+ chat_id TEXT NOT NULL,
611498
+ chat_type TEXT,
611499
+ chat_title TEXT,
611500
+ message_id INTEGER NOT NULL,
611501
+ message_thread_id INTEGER,
611502
+ update_id INTEGER,
611503
+ telegram_date INTEGER,
611504
+ received_at INTEGER NOT NULL,
611505
+ role TEXT NOT NULL DEFAULT 'user',
611506
+ from_user_id INTEGER,
611507
+ username TEXT,
611508
+ first_name TEXT,
611509
+ text TEXT,
611510
+ raw_json TEXT NOT NULL,
611511
+ normalized_json TEXT NOT NULL,
611512
+ reply_to_message_id INTEGER,
611513
+ reply_to_username TEXT,
611514
+ media_json TEXT,
611515
+ PRIMARY KEY (chat_id, message_id, role)
611516
+ );
611517
+ CREATE INDEX IF NOT EXISTS idx_telegram_messages_session_time ON telegram_messages(session_key, received_at);
611518
+ CREATE INDEX IF NOT EXISTS idx_telegram_messages_user_time ON telegram_messages(session_key, from_user_id, received_at);
611519
+ CREATE INDEX IF NOT EXISTS idx_telegram_messages_username_time ON telegram_messages(session_key, username, received_at);
611520
+ CREATE VIRTUAL TABLE IF NOT EXISTS telegram_messages_fts USING fts5(
611521
+ session_key UNINDEXED,
611522
+ chat_id UNINDEXED,
611523
+ message_id UNINDEXED,
611524
+ username,
611525
+ first_name,
611526
+ text,
611527
+ content='telegram_messages',
611528
+ content_rowid='rowid'
611529
+ );
611530
+ CREATE TRIGGER IF NOT EXISTS telegram_messages_ai AFTER INSERT ON telegram_messages BEGIN
611531
+ INSERT INTO telegram_messages_fts(rowid, session_key, chat_id, message_id, username, first_name, text)
611532
+ VALUES (new.rowid, new.session_key, new.chat_id, new.message_id, new.username, new.first_name, new.text);
611533
+ END;
611534
+ CREATE TRIGGER IF NOT EXISTS telegram_messages_ad AFTER DELETE ON telegram_messages BEGIN
611535
+ INSERT INTO telegram_messages_fts(telegram_messages_fts, rowid, session_key, chat_id, message_id, username, first_name, text)
611536
+ VALUES('delete', old.rowid, old.session_key, old.chat_id, old.message_id, old.username, old.first_name, old.text);
611537
+ END;
611538
+ CREATE TRIGGER IF NOT EXISTS telegram_messages_au AFTER UPDATE ON telegram_messages BEGIN
611539
+ INSERT INTO telegram_messages_fts(telegram_messages_fts, rowid, session_key, chat_id, message_id, username, first_name, text)
611540
+ VALUES('delete', old.rowid, old.session_key, old.chat_id, old.message_id, old.username, old.first_name, old.text);
611541
+ INSERT INTO telegram_messages_fts(rowid, session_key, chat_id, message_id, username, first_name, text)
611542
+ VALUES (new.rowid, new.session_key, new.chat_id, new.message_id, new.username, new.first_name, new.text);
611543
+ END;
611544
+ `);
611545
+ this.telegramSqliteDb = db;
611546
+ return db;
611547
+ } catch {
611548
+ this.telegramSqliteDb = false;
611549
+ return null;
611550
+ }
611551
+ }
611552
+ persistTelegramRawMessage(update2, msg) {
611553
+ const db = this.telegramDb();
611554
+ if (!db) return;
611555
+ const rawMessage = update2.message ?? update2.guest_message ?? update2.edited_message ?? update2.channel_post ?? {};
611556
+ const sessionKey = this.sessionKeyForMessage(msg);
611557
+ const media = msg.media || msg.replyToMedia || msg.livePhoto;
611558
+ try {
611559
+ db.prepare(`
611560
+ INSERT INTO telegram_messages (
611561
+ session_key, chat_id, chat_type, chat_title, message_id, message_thread_id,
611562
+ update_id, telegram_date, received_at, role, from_user_id, username, first_name,
611563
+ text, raw_json, normalized_json, reply_to_message_id, reply_to_username, media_json
611564
+ ) VALUES (
611565
+ @session_key, @chat_id, @chat_type, @chat_title, @message_id, @message_thread_id,
611566
+ @update_id, @telegram_date, @received_at, 'user', @from_user_id, @username, @first_name,
611567
+ @text, @raw_json, @normalized_json, @reply_to_message_id, @reply_to_username, @media_json
611568
+ )
611569
+ ON CONFLICT(chat_id, message_id, role) DO UPDATE SET
611570
+ session_key=excluded.session_key,
611571
+ chat_type=excluded.chat_type,
611572
+ chat_title=excluded.chat_title,
611573
+ message_thread_id=excluded.message_thread_id,
611574
+ update_id=excluded.update_id,
611575
+ telegram_date=excluded.telegram_date,
611576
+ received_at=excluded.received_at,
611577
+ from_user_id=excluded.from_user_id,
611578
+ username=excluded.username,
611579
+ first_name=excluded.first_name,
611580
+ text=excluded.text,
611581
+ raw_json=excluded.raw_json,
611582
+ normalized_json=excluded.normalized_json,
611583
+ reply_to_message_id=excluded.reply_to_message_id,
611584
+ reply_to_username=excluded.reply_to_username,
611585
+ media_json=excluded.media_json
611586
+ `).run({
611587
+ session_key: sessionKey,
611588
+ chat_id: String(msg.chatId),
611589
+ chat_type: msg.chatType,
611590
+ chat_title: msg.chatTitle ?? null,
611591
+ message_id: msg.messageId,
611592
+ message_thread_id: msg.messageThreadId ?? null,
611593
+ update_id: typeof update2.update_id === "number" ? update2.update_id : null,
611594
+ telegram_date: typeof rawMessage.date === "number" ? rawMessage.date : null,
611595
+ received_at: Date.now(),
611596
+ from_user_id: msg.fromUserId ?? null,
611597
+ username: msg.username || null,
611598
+ first_name: msg.firstName || null,
611599
+ text: msg.text || "",
611600
+ raw_json: JSON.stringify(rawMessage),
611601
+ normalized_json: JSON.stringify(msg),
611602
+ reply_to_message_id: msg.replyToMessageId ?? null,
611603
+ reply_to_username: msg.replyToUsername || null,
611604
+ media_json: media ? JSON.stringify(media) : null
611605
+ });
611606
+ } catch {
611607
+ }
611608
+ }
611609
+ persistTelegramAssistantMessage(msg, text, messageId, replyToMessageId) {
611610
+ if (!messageId) return;
611611
+ const db = this.telegramDb();
611612
+ if (!db) return;
611613
+ const sessionKey = this.sessionKeyForMessage(msg);
611614
+ const now = Date.now();
611615
+ const normalized = {
611616
+ role: "assistant",
611617
+ chatId: msg.chatId,
611618
+ chatType: msg.chatType,
611619
+ chatTitle: msg.chatTitle,
611620
+ messageId,
611621
+ messageThreadId: msg.messageThreadId,
611622
+ replyToMessageId,
611623
+ username: this.state.botUsername || "omnius",
611624
+ text
611625
+ };
611626
+ try {
611627
+ db.prepare(`
611628
+ INSERT INTO telegram_messages (
611629
+ session_key, chat_id, chat_type, chat_title, message_id, message_thread_id,
611630
+ update_id, telegram_date, received_at, role, from_user_id, username, first_name,
611631
+ text, raw_json, normalized_json, reply_to_message_id, reply_to_username, media_json
611632
+ ) VALUES (
611633
+ @session_key, @chat_id, @chat_type, @chat_title, @message_id, @message_thread_id,
611634
+ NULL, NULL, @received_at, 'assistant', NULL, @username, 'Omnius',
611635
+ @text, @raw_json, @normalized_json, @reply_to_message_id, NULL, NULL
611636
+ )
611637
+ ON CONFLICT(chat_id, message_id, role) DO UPDATE SET
611638
+ session_key=excluded.session_key,
611639
+ chat_type=excluded.chat_type,
611640
+ chat_title=excluded.chat_title,
611641
+ message_thread_id=excluded.message_thread_id,
611642
+ received_at=excluded.received_at,
611643
+ username=excluded.username,
611644
+ text=excluded.text,
611645
+ raw_json=excluded.raw_json,
611646
+ normalized_json=excluded.normalized_json,
611647
+ reply_to_message_id=excluded.reply_to_message_id
611648
+ `).run({
611649
+ session_key: sessionKey,
611650
+ chat_id: String(msg.chatId),
611651
+ chat_type: msg.chatType,
611652
+ chat_title: msg.chatTitle ?? null,
611653
+ message_id: messageId,
611654
+ message_thread_id: msg.messageThreadId ?? null,
611655
+ received_at: now,
611656
+ username: this.state.botUsername || "omnius",
611657
+ text,
611658
+ raw_json: JSON.stringify(normalized),
611659
+ normalized_json: JSON.stringify(normalized),
611660
+ reply_to_message_id: replyToMessageId ?? null
611661
+ });
611662
+ } catch {
611663
+ }
611664
+ }
611665
+ createTelegramAssociativeMemory() {
611666
+ const now = Date.now();
611667
+ return {
611668
+ version: 1,
611669
+ createdAt: now,
611670
+ updatedAt: now,
611671
+ facts: [],
611672
+ users: {},
611673
+ relationships: [],
611674
+ actions: []
611675
+ };
611676
+ }
611677
+ normalizeTelegramAssociativeMemory(raw) {
611678
+ const created = this.createTelegramAssociativeMemory();
611679
+ const users = {};
611680
+ const rawUsers = raw.users && typeof raw.users === "object" ? raw.users : {};
611681
+ for (const [key, user] of Object.entries(rawUsers)) {
611682
+ if (!user || typeof user !== "object") continue;
611683
+ users[key] = {
611684
+ username: String(user.username || "unknown"),
611685
+ displayName: user.displayName,
611686
+ userId: typeof user.userId === "number" ? user.userId : void 0,
611687
+ aliases: Array.isArray(user.aliases) ? user.aliases.map(String).slice(0, 20) : [],
611688
+ firstSeenTs: typeof user.firstSeenTs === "number" ? user.firstSeenTs : created.createdAt,
611689
+ lastSeenTs: typeof user.lastSeenTs === "number" ? user.lastSeenTs : created.updatedAt,
611690
+ messageCount: typeof user.messageCount === "number" ? user.messageCount : 0,
611691
+ directAddressCount: typeof user.directAddressCount === "number" ? user.directAddressCount : 0,
611692
+ replyCount: typeof user.replyCount === "number" ? user.replyCount : 0,
611693
+ toneTags: Array.isArray(user.toneTags) ? user.toneTags.map(String).slice(0, 20) : [],
611694
+ facts: Array.isArray(user.facts) ? user.facts.slice(0, TELEGRAM_ASSOCIATIVE_USER_FACT_LIMIT).map((fact) => this.normalizeTelegramAssociativeFact(fact)) : [],
611695
+ relationshipHints: Array.isArray(user.relationshipHints) ? user.relationshipHints.map(String).slice(0, 80) : [],
611696
+ recentTopics: Array.isArray(user.recentTopics) ? user.recentTopics.map(String).slice(0, 80) : [],
611697
+ lastMessages: Array.isArray(user.lastMessages) ? user.lastMessages.map(String).slice(-40) : []
611698
+ };
611699
+ }
611700
+ return {
611701
+ version: typeof raw.version === "number" ? raw.version : 1,
611702
+ createdAt: typeof raw.createdAt === "number" ? raw.createdAt : created.createdAt,
611703
+ updatedAt: typeof raw.updatedAt === "number" ? raw.updatedAt : created.updatedAt,
611704
+ facts: Array.isArray(raw.facts) ? raw.facts.slice(0, TELEGRAM_ASSOCIATIVE_FACT_LIMIT).map((fact) => this.normalizeTelegramAssociativeFact(fact)) : [],
611705
+ users,
611706
+ relationships: Array.isArray(raw.relationships) ? raw.relationships.slice(0, TELEGRAM_ASSOCIATIVE_RELATION_LIMIT).map((fact) => this.normalizeTelegramAssociativeFact(fact)) : [],
611707
+ actions: Array.isArray(raw.actions) ? raw.actions.slice(-TELEGRAM_ASSOCIATIVE_ACTION_LIMIT).map((action) => ({
611708
+ id: String(action.id || createHash23("sha1").update(JSON.stringify(action)).digest("hex").slice(0, 12)),
611709
+ ts: typeof action.ts === "number" ? action.ts : Date.now(),
611710
+ role: action.role === "assistant" ? "assistant" : "user",
611711
+ speaker: String(action.speaker || "unknown"),
611712
+ mode: action.mode,
611713
+ text: String(action.text || ""),
611714
+ messageId: typeof action.messageId === "number" ? action.messageId : void 0,
611715
+ replyToMessageId: typeof action.replyToMessageId === "number" ? action.replyToMessageId : void 0,
611716
+ userId: typeof action.userId === "number" ? action.userId : void 0,
611717
+ username: typeof action.username === "string" ? action.username : void 0
611718
+ })) : []
611719
+ };
611720
+ }
611721
+ normalizeTelegramAssociativeFact(raw) {
611722
+ const text = String(raw.text || "").trim();
611723
+ const now = Date.now();
611724
+ return {
611725
+ id: String(raw.id || createHash23("sha1").update(text || String(now)).digest("hex").slice(0, 12)),
611726
+ text,
611727
+ tags: Array.isArray(raw.tags) ? raw.tags.map(String).slice(0, 16) : [],
611728
+ speakers: Array.isArray(raw.speakers) ? raw.speakers.map(String).slice(0, 16) : [],
611729
+ userIds: Array.isArray(raw.userIds) ? raw.userIds.filter((id) => typeof id === "number").slice(0, 32) : [],
611730
+ usernames: Array.isArray(raw.usernames) ? raw.usernames.map(String).slice(0, 32) : [],
611731
+ messageIds: Array.isArray(raw.messageIds) ? raw.messageIds.filter((id) => typeof id === "number").slice(0, 80) : [],
611732
+ createdAt: typeof raw.createdAt === "number" ? raw.createdAt : now,
611733
+ updatedAt: typeof raw.updatedAt === "number" ? raw.updatedAt : now,
611734
+ weight: typeof raw.weight === "number" ? raw.weight : 1
611735
+ };
611736
+ }
611737
+ telegramAssociativeMemoryForSession(sessionKey) {
611738
+ this.ensureTelegramConversationLoaded(sessionKey);
611739
+ let memory = this.chatAssociativeMemory.get(sessionKey);
611740
+ if (!memory) {
611741
+ memory = this.createTelegramAssociativeMemory();
611742
+ this.chatAssociativeMemory.set(sessionKey, memory);
611743
+ }
611744
+ return memory;
611745
+ }
611229
611746
  telegramPersonalityScope(sessionKey, msg) {
611230
611747
  const label = msg.chatType !== "private" ? `${msg.chatTitle || msg.chatType}-${String(msg.chatId)}` : `private-${msg.username || msg.fromUserId || msg.chatId}`;
611231
611748
  return {
@@ -611242,8 +611759,9 @@ ${mediaContext}` : ""
611242
611759
  if (!existsSync112(path12)) return;
611243
611760
  try {
611244
611761
  const parsed = JSON.parse(readFileSync92(path12, "utf8"));
611762
+ const loadedHistory = Array.isArray(parsed.history) ? parsed.history : [];
611245
611763
  if (Array.isArray(parsed.history)) {
611246
- this.chatHistory.set(sessionKey, parsed.history.slice(-TELEGRAM_CHAT_HISTORY_LIMIT));
611764
+ this.chatHistory.set(sessionKey, loadedHistory.slice(-TELEGRAM_CHAT_HISTORY_LIMIT));
611247
611765
  }
611248
611766
  if (Array.isArray(parsed.participants)) {
611249
611767
  const participants = /* @__PURE__ */ new Map();
@@ -611260,6 +611778,17 @@ ${mediaContext}` : ""
611260
611778
  if (Array.isArray(parsed.memoryCards)) {
611261
611779
  this.chatMemoryCards.set(sessionKey, parsed.memoryCards.slice(0, TELEGRAM_MEMORY_CARD_LIMIT));
611262
611780
  }
611781
+ if (parsed.associativeMemory) {
611782
+ this.chatAssociativeMemory.set(
611783
+ sessionKey,
611784
+ this.normalizeTelegramAssociativeMemory(parsed.associativeMemory)
611785
+ );
611786
+ } else if (loadedHistory.length > 0) {
611787
+ this.hydrateTelegramAssociativeMemoryFromHistory(sessionKey, loadedHistory);
611788
+ }
611789
+ if (loadedHistory.length > 0) {
611790
+ this.backfillTelegramLoadedHistory(sessionKey, loadedHistory);
611791
+ }
611263
611792
  if (parsed.stimulation) {
611264
611793
  this.stimulation.setState(sessionKey, parsed.stimulation);
611265
611794
  }
@@ -611273,6 +611802,108 @@ ${mediaContext}` : ""
611273
611802
  } catch {
611274
611803
  }
611275
611804
  }
611805
+ hydrateTelegramAssociativeMemoryFromHistory(sessionKey, history) {
611806
+ const memory = this.createTelegramAssociativeMemory();
611807
+ this.chatAssociativeMemory.set(sessionKey, memory);
611808
+ for (const entry of history) {
611809
+ this.updateTelegramAssociativeMemory(sessionKey, entry);
611810
+ }
611811
+ }
611812
+ telegramHistoryBackfillMessageId(sessionKey, entry, index) {
611813
+ if (typeof entry.messageId === "number" && Number.isFinite(entry.messageId)) return entry.messageId;
611814
+ const digest3 = createHash23("sha1").update(`${sessionKey}:${index}:${entry.role}:${entry.ts ?? ""}:${entry.text}`).digest("hex").slice(0, 8);
611815
+ return -Number.parseInt(digest3, 16);
611816
+ }
611817
+ backfillTelegramLoadedHistory(sessionKey, history) {
611818
+ if (!this.repoRoot || history.length === 0) return;
611819
+ const db = this.telegramDb();
611820
+ if (db) {
611821
+ try {
611822
+ const existing = db.prepare("SELECT COUNT(*) AS n FROM telegram_messages WHERE session_key = ?").get(sessionKey);
611823
+ if ((existing?.n ?? 0) < history.length) {
611824
+ const insert = db.prepare(`
611825
+ INSERT INTO telegram_messages (
611826
+ session_key, chat_id, chat_type, chat_title, message_id, message_thread_id,
611827
+ update_id, telegram_date, received_at, role, from_user_id, username, first_name,
611828
+ text, raw_json, normalized_json, reply_to_message_id, reply_to_username, media_json
611829
+ ) VALUES (
611830
+ @session_key, @chat_id, @chat_type, @chat_title, @message_id, @message_thread_id,
611831
+ NULL, NULL, @received_at, @role, @from_user_id, @username, @first_name,
611832
+ @text, @raw_json, @normalized_json, @reply_to_message_id, @reply_to_username, @media_json
611833
+ )
611834
+ ON CONFLICT(chat_id, message_id, role) DO UPDATE SET
611835
+ session_key=excluded.session_key,
611836
+ chat_type=excluded.chat_type,
611837
+ chat_title=excluded.chat_title,
611838
+ message_thread_id=excluded.message_thread_id,
611839
+ received_at=excluded.received_at,
611840
+ from_user_id=excluded.from_user_id,
611841
+ username=excluded.username,
611842
+ first_name=excluded.first_name,
611843
+ text=excluded.text,
611844
+ raw_json=excluded.raw_json,
611845
+ normalized_json=excluded.normalized_json,
611846
+ reply_to_message_id=excluded.reply_to_message_id,
611847
+ reply_to_username=excluded.reply_to_username,
611848
+ media_json=excluded.media_json
611849
+ `);
611850
+ const transaction = db.transaction((entries) => {
611851
+ entries.forEach((entry, index) => {
611852
+ const messageId = this.telegramHistoryBackfillMessageId(sessionKey, entry, index);
611853
+ const chatId = entry.chatId !== void 0 ? String(entry.chatId) : sessionKey.replace(/^chat:/, "");
611854
+ insert.run({
611855
+ session_key: sessionKey,
611856
+ chat_id: chatId || "unknown",
611857
+ chat_type: entry.chatType || null,
611858
+ chat_title: entry.chatTitle || null,
611859
+ message_id: messageId,
611860
+ message_thread_id: entry.messageThreadId ?? null,
611861
+ received_at: entry.ts ?? Date.now(),
611862
+ role: entry.role === "assistant" ? "assistant" : "user",
611863
+ from_user_id: entry.fromUserId ?? null,
611864
+ username: entry.role === "assistant" ? this.state.botUsername || "omnius" : entry.username || null,
611865
+ first_name: entry.role === "assistant" ? "Omnius" : entry.firstName || null,
611866
+ text: entry.text || "",
611867
+ raw_json: JSON.stringify({ source: "telegram-conversations", sessionKey, entry }),
611868
+ normalized_json: JSON.stringify({ ...entry, messageId }),
611869
+ reply_to_message_id: entry.replyToMessageId ?? null,
611870
+ reply_to_username: entry.replyContext?.sender?.username || null,
611871
+ media_json: entry.mediaSummary ? JSON.stringify({ summary: entry.mediaSummary }) : null
611872
+ });
611873
+ });
611874
+ });
611875
+ transaction(history);
611876
+ try {
611877
+ db.exec("INSERT INTO telegram_messages_fts(telegram_messages_fts) VALUES('rebuild')");
611878
+ } catch {
611879
+ }
611880
+ }
611881
+ } catch {
611882
+ }
611883
+ }
611884
+ try {
611885
+ const paths = omniusMemoryDbPaths(this.repoRoot);
611886
+ const graph = new TemporalGraph(paths.knowledge);
611887
+ const store2 = new EpisodeStore(paths.episodes, graph);
611888
+ let existingTelegramEpisodes = 0;
611889
+ try {
611890
+ const row = store2.getDb().prepare(`
611891
+ SELECT COUNT(*) AS n FROM episodes
611892
+ WHERE session_id = ? AND metadata LIKE '%"sourceSurface":"telegram"%'
611893
+ `).get(sessionKey);
611894
+ existingTelegramEpisodes = row?.n ?? 0;
611895
+ } finally {
611896
+ store2.close();
611897
+ graph.close();
611898
+ }
611899
+ if (existingTelegramEpisodes < history.length) {
611900
+ for (const entry of history) {
611901
+ this.upsertTelegramReflectionHistoryEntry(sessionKey, entry);
611902
+ }
611903
+ }
611904
+ } catch {
611905
+ }
611906
+ }
611276
611907
  ensureAllTelegramConversationsLoaded() {
611277
611908
  if (this.loadedAllConversationState) return;
611278
611909
  this.loadedAllConversationState = true;
@@ -611304,6 +611935,7 @@ ${mediaContext}` : ""
611304
611935
  history: this.chatHistory.get(sessionKey) ?? [],
611305
611936
  participants,
611306
611937
  memoryCards: this.chatMemoryCards.get(sessionKey) ?? [],
611938
+ associativeMemory: this.telegramAssociativeMemoryForSession(sessionKey),
611307
611939
  stimulation: this.stimulation.getState(sessionKey),
611308
611940
  reflection: this.channelReflectionState.get(sessionKey) ?? { autoFollowup: false }
611309
611941
  };
@@ -611331,7 +611963,8 @@ ${mediaContext}` : ""
611331
611963
  };
611332
611964
  }
611333
611965
  buildTelegramChannelDaydreamInput(sessionKey, nowMs = Date.now()) {
611334
- const history = this.chatHistory.get(sessionKey) ?? [];
611966
+ const sqliteHistory = this.telegramSqliteHistoryForSession(sessionKey, 1500);
611967
+ const history = sqliteHistory.length > 0 ? sqliteHistory : this.chatHistory.get(sessionKey) ?? [];
611335
611968
  if (history.length === 0) return null;
611336
611969
  const last2 = [...history].reverse().find((entry) => entry.chatId !== void 0);
611337
611970
  if (!last2) return null;
@@ -611636,6 +612269,7 @@ ${mediaContext}` : ""
611636
612269
  this.upsertTelegramReflectionHistoryEntry(sessionKey, entry);
611637
612270
  this.updateTelegramParticipantProfile(sessionKey, msg, text);
611638
612271
  this.updateTelegramMemoryCards(sessionKey, entry);
612272
+ this.updateTelegramAssociativeMemory(sessionKey, entry);
611639
612273
  try {
611640
612274
  updateScopedPersonality(this.telegramPersonalityScope(sessionKey, msg), {
611641
612275
  speaker: telegramSpeakerLabel(msg),
@@ -611669,9 +612303,16 @@ ${mediaContext}` : ""
611669
612303
  chatTitle: msg.chatTitle
611670
612304
  };
611671
612305
  this.recordChatHistory(sessionKey, entry);
612306
+ this.persistTelegramAssistantMessage(
612307
+ msg,
612308
+ clean5,
612309
+ options2.messageId ?? void 0,
612310
+ options2.replyToMessageId
612311
+ );
611672
612312
  this.upsertTelegramReflectionHistoryEntry(sessionKey, entry);
611673
612313
  this.stimulation.recordAgentOutput(sessionKey);
611674
612314
  this.updateTelegramMemoryCards(sessionKey, entry);
612315
+ this.updateTelegramAssociativeMemory(sessionKey, entry);
611675
612316
  try {
611676
612317
  updateScopedPersonality(this.telegramPersonalityScope(sessionKey, msg), {
611677
612318
  speaker: entry.speaker || "Assistant",
@@ -611862,6 +612503,160 @@ ${mediaContext}` : ""
611862
612503
  participants.set(participantKey, profile);
611863
612504
  this.chatParticipants.set(sessionKey, participants);
611864
612505
  }
612506
+ updateTelegramAssociativeMemory(sessionKey, entry) {
612507
+ const memory = this.telegramAssociativeMemoryForSession(sessionKey);
612508
+ const now = entry.ts ?? Date.now();
612509
+ memory.updatedAt = now;
612510
+ const speaker = telegramHistorySpeaker(entry);
612511
+ const actionId = createHash23("sha1").update(`${sessionKey}:${entry.role}:${entry.messageId ?? ""}:${now}:${entry.text}`).digest("hex").slice(0, 16);
612512
+ if (!memory.actions.some((action) => action.id === actionId)) {
612513
+ memory.actions.push({
612514
+ id: actionId,
612515
+ ts: now,
612516
+ role: entry.role,
612517
+ speaker,
612518
+ mode: entry.mode,
612519
+ text: truncateTelegramContextLine(entry.text, 900),
612520
+ messageId: entry.messageId,
612521
+ replyToMessageId: entry.replyToMessageId,
612522
+ userId: entry.fromUserId,
612523
+ username: entry.username
612524
+ });
612525
+ if (memory.actions.length > TELEGRAM_ASSOCIATIVE_ACTION_LIMIT) {
612526
+ memory.actions.splice(
612527
+ 0,
612528
+ memory.actions.length - TELEGRAM_ASSOCIATIVE_ACTION_LIMIT
612529
+ );
612530
+ }
612531
+ }
612532
+ if (entry.role === "user") {
612533
+ const userKey = String(entry.fromUserId || entry.username || entry.firstName || speaker);
612534
+ const existing = memory.users[userKey];
612535
+ const userMemory = existing ?? {
612536
+ userId: entry.fromUserId,
612537
+ username: entry.username || "unknown",
612538
+ displayName: entry.firstName || entry.username || speaker,
612539
+ aliases: [],
612540
+ firstSeenTs: now,
612541
+ lastSeenTs: now,
612542
+ messageCount: 0,
612543
+ directAddressCount: 0,
612544
+ replyCount: 0,
612545
+ toneTags: [],
612546
+ facts: [],
612547
+ relationshipHints: [],
612548
+ recentTopics: [],
612549
+ lastMessages: []
612550
+ };
612551
+ userMemory.userId = entry.fromUserId ?? userMemory.userId;
612552
+ userMemory.username = entry.username || userMemory.username;
612553
+ userMemory.displayName = entry.firstName || userMemory.displayName;
612554
+ for (const alias of [entry.username, entry.firstName, speaker].filter(Boolean)) {
612555
+ const clean5 = alias.replace(/^@/, "").trim();
612556
+ if (clean5 && !userMemory.aliases.includes(clean5)) userMemory.aliases.push(clean5);
612557
+ }
612558
+ userMemory.aliases = userMemory.aliases.slice(0, 20);
612559
+ userMemory.lastSeenTs = now;
612560
+ userMemory.messageCount += 1;
612561
+ if (entry.replyToMessageId) userMemory.replyCount += 1;
612562
+ if (this.state.botUsername && entry.text.toLowerCase().includes(`@${this.state.botUsername.toLowerCase()}`)) {
612563
+ userMemory.directAddressCount += 1;
612564
+ }
612565
+ for (const tag of inferTelegramToneTags(entry.text)) {
612566
+ if (!userMemory.toneTags.includes(tag)) userMemory.toneTags.push(tag);
612567
+ }
612568
+ userMemory.toneTags = userMemory.toneTags.slice(0, 20);
612569
+ const compact2 = truncateTelegramContextLine(entry.text, 240);
612570
+ if (compact2) {
612571
+ userMemory.lastMessages.push(compact2);
612572
+ userMemory.lastMessages = userMemory.lastMessages.slice(-40);
612573
+ }
612574
+ for (const topic of telegramMemoryTags(entry.text, entry.mediaSummary).slice(0, 6)) {
612575
+ if (!userMemory.recentTopics.includes(topic)) userMemory.recentTopics.push(topic);
612576
+ }
612577
+ userMemory.recentTopics = userMemory.recentTopics.slice(-80);
612578
+ const facts = this.extractTelegramAssociativeFacts(entry, speaker);
612579
+ for (const text of facts) {
612580
+ const fact = this.upsertTelegramAssociativeFact(memory.facts, text, entry, speaker, 1.5);
612581
+ this.upsertTelegramAssociativeFact(userMemory.facts, fact.text, entry, speaker, fact.weight);
612582
+ }
612583
+ if (entry.replyContext?.sender || entry.replyToMessageId) {
612584
+ const target = entry.replyContext?.sender ? telegramReplySenderLabel(entry.replyContext.sender) : `message ${entry.replyToMessageId}`;
612585
+ const hint = `${speaker} replied to ${target}: ${truncateTelegramContextLine(entry.text, 180)}`;
612586
+ if (!userMemory.relationshipHints.includes(hint)) {
612587
+ userMemory.relationshipHints.push(hint);
612588
+ userMemory.relationshipHints = userMemory.relationshipHints.slice(-80);
612589
+ }
612590
+ this.upsertTelegramAssociativeFact(memory.relationships, hint, entry, speaker, 1.2);
612591
+ }
612592
+ userMemory.facts.sort((a2, b) => b.weight - a2.weight || b.updatedAt - a2.updatedAt);
612593
+ userMemory.facts = userMemory.facts.slice(0, TELEGRAM_ASSOCIATIVE_USER_FACT_LIMIT);
612594
+ memory.users[userKey] = userMemory;
612595
+ }
612596
+ memory.facts.sort((a2, b) => b.weight - a2.weight || b.updatedAt - a2.updatedAt);
612597
+ memory.facts = memory.facts.slice(0, TELEGRAM_ASSOCIATIVE_FACT_LIMIT);
612598
+ memory.relationships.sort((a2, b) => b.updatedAt - a2.updatedAt);
612599
+ memory.relationships = memory.relationships.slice(0, TELEGRAM_ASSOCIATIVE_RELATION_LIMIT);
612600
+ }
612601
+ extractTelegramAssociativeFacts(entry, speaker) {
612602
+ if (entry.role !== "user") return [];
612603
+ const text = truncateTelegramContextLine(entry.text, 500);
612604
+ const facts = /* @__PURE__ */ new Set();
612605
+ const patterns = [
612606
+ /\b(?:remember|note|keep in mind|for future reference)\s+(?:that\s+)?(.{4,260})/i,
612607
+ /\bmy name is\s+(.{2,120})/i,
612608
+ /\bcall me\s+(.{2,120})/i,
612609
+ /\bi\s+(?:am|work as|live in|like|love|prefer|use|run|have|need|want)\b.{0,220}/i,
612610
+ /\b(?:we|this group|our)\s+.{0,220}\b(?:is|are|uses|prefers|likes|works|has|needs)\b.{0,220}/i
612611
+ ];
612612
+ for (const pattern of patterns) {
612613
+ const match = text.match(pattern);
612614
+ if (!match) continue;
612615
+ const body = (match[1] || match[0]).trim().replace(/[.!?]*$/, "");
612616
+ if (body.length >= 3) facts.add(`${speaker}: ${body}`);
612617
+ }
612618
+ if (entry.mediaSummary) {
612619
+ facts.add(`${speaker} shared media: ${entry.mediaSummary}`);
612620
+ }
612621
+ return [...facts].slice(0, 8);
612622
+ }
612623
+ upsertTelegramAssociativeFact(facts, text, entry, speaker, weight = 1) {
612624
+ const clean5 = truncateTelegramContextLine(text, 500);
612625
+ const key = clean5.toLowerCase();
612626
+ const now = entry.ts ?? Date.now();
612627
+ let fact = facts.find((item) => item.text.toLowerCase() === key);
612628
+ if (!fact) {
612629
+ fact = {
612630
+ id: createHash23("sha1").update(`${entry.chatId ?? ""}:${key}`).digest("hex").slice(0, 12),
612631
+ text: clean5,
612632
+ tags: telegramMemoryTags(clean5, entry.mediaSummary),
612633
+ speakers: [],
612634
+ userIds: [],
612635
+ usernames: [],
612636
+ messageIds: [],
612637
+ createdAt: now,
612638
+ updatedAt: now,
612639
+ weight
612640
+ };
612641
+ facts.push(fact);
612642
+ }
612643
+ fact.updatedAt = now;
612644
+ fact.weight = Math.min(10, Math.max(fact.weight, weight) + 0.2);
612645
+ if (!fact.speakers.includes(speaker)) fact.speakers.push(speaker);
612646
+ fact.speakers = fact.speakers.slice(0, 16);
612647
+ if (entry.fromUserId && !fact.userIds.includes(entry.fromUserId)) fact.userIds.push(entry.fromUserId);
612648
+ fact.userIds = fact.userIds.slice(0, 32);
612649
+ const username = (entry.username || "").replace(/^@/, "").toLowerCase();
612650
+ if (username && !fact.usernames.includes(username)) fact.usernames.push(username);
612651
+ fact.usernames = fact.usernames.slice(0, 32);
612652
+ if (entry.messageId && !fact.messageIds.includes(entry.messageId)) fact.messageIds.push(entry.messageId);
612653
+ fact.messageIds = fact.messageIds.slice(-80);
612654
+ for (const tag of telegramMemoryTags(clean5, entry.mediaSummary)) {
612655
+ if (!fact.tags.includes(tag)) fact.tags.push(tag);
612656
+ }
612657
+ fact.tags = fact.tags.slice(0, 16);
612658
+ return fact;
612659
+ }
611865
612660
  updateTelegramMemoryCards(sessionKey, entry) {
611866
612661
  const text = truncateTelegramContextLine(entry.text, 500);
611867
612662
  if (!text || text.length < 3) return;
@@ -611955,6 +612750,249 @@ ${mediaContext}` : ""
611955
612750
  )
611956
612751
  })).sort((a2, b) => b.score - a2.score || b.card.updatedAt - a2.card.updatedAt).slice(0, limit);
611957
612752
  }
612753
+ relevantTelegramAssociativeMemoryContext(sessionKey, msg, limit = 12) {
612754
+ const memory = this.chatAssociativeMemory.get(sessionKey);
612755
+ if (!memory) return "";
612756
+ const queryTokens = telegramMemoryTokens([
612757
+ telegramSpeakerLabel(msg),
612758
+ msg.username || "",
612759
+ msg.firstName || "",
612760
+ msg.text,
612761
+ summarizeTelegramMessageAttachments(msg)
612762
+ ].join(" "));
612763
+ const currentUsername = (msg.username || "").replace(/^@/, "").toLowerCase();
612764
+ const userEntries = Object.entries(memory.users).filter(
612765
+ ([, user]) => msg.fromUserId !== void 0 && user.userId === msg.fromUserId || !!currentUsername && user.username.replace(/^@/, "").toLowerCase() === currentUsername || user.aliases.some((alias) => alias.replace(/^@/, "").toLowerCase() === currentUsername)
612766
+ );
612767
+ const scoredFacts = memory.facts.map((fact) => ({
612768
+ fact,
612769
+ score: telegramMemorySimilarity(
612770
+ queryTokens,
612771
+ telegramMemoryTokens([fact.text, fact.tags.join(" "), fact.speakers.join(" ")].join(" "))
612772
+ ) + (fact.userIds.includes(msg.fromUserId ?? -1) ? 0.35 : 0) + (currentUsername && fact.usernames.includes(currentUsername) ? 0.35 : 0)
612773
+ })).filter((item) => item.score > 0).sort((a2, b) => b.score - a2.score || b.fact.updatedAt - a2.fact.updatedAt).slice(0, limit);
612774
+ const relationshipFacts = memory.relationships.filter(
612775
+ (fact) => fact.userIds.includes(msg.fromUserId ?? -1) || !!currentUsername && fact.usernames.includes(currentUsername)
612776
+ ).slice(0, 6);
612777
+ const recentActions = memory.actions.filter(
612778
+ (action) => action.userId === msg.fromUserId || !!currentUsername && action.username?.replace(/^@/, "").toLowerCase() === currentUsername || action.role === "assistant"
612779
+ ).slice(-8);
612780
+ const sections = [];
612781
+ if (userEntries.length > 0) {
612782
+ const lines = userEntries.map(([, user]) => {
612783
+ const facts = user.facts.slice(0, 6).map((fact) => ` - ${telegramContextJsonString(fact.text, 220)}`).join("\n");
612784
+ const hints = user.relationshipHints.slice(-4).map((hint) => ` - relation=${telegramContextJsonString(hint, 200)}`).join("\n");
612785
+ const topics = user.recentTopics.slice(-8).join(", ") || "none";
612786
+ return [
612787
+ `- ${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}`,
612788
+ facts,
612789
+ hints
612790
+ ].filter(Boolean).join("\n");
612791
+ });
612792
+ sections.push(`### Durable Associative User Memory
612793
+ ${lines.join("\n")}`);
612794
+ }
612795
+ if (scoredFacts.length > 0) {
612796
+ const lines = scoredFacts.map(
612797
+ ({ fact, score }) => `- (${score.toFixed(2)}) ${telegramContextJsonString(fact.text, 260)}${fact.messageIds.length ? ` [messages:${fact.messageIds.slice(-4).join(",")}]` : ""}`
612798
+ );
612799
+ sections.push(`### Durable Associative Memory Recall
612800
+ ${lines.join("\n")}`);
612801
+ }
612802
+ if (relationshipFacts.length > 0) {
612803
+ const lines = relationshipFacts.map((fact) => `- ${telegramContextJsonString(fact.text, 240)}`);
612804
+ sections.push(`### Durable Relationship Recall
612805
+ ${lines.join("\n")}`);
612806
+ }
612807
+ if (recentActions.length > 0) {
612808
+ const lines = recentActions.map(
612809
+ (action) => `- ${telegramHistoryTime({ ts: action.ts, role: action.role, text: action.text })} ${action.speaker}/${action.role}${action.mode ? `/${action.mode}` : ""}: ${telegramContextJsonString(action.text, 220)}`
612810
+ );
612811
+ sections.push(`### Durable Recent Action Ledger Recall
612812
+ ${lines.join("\n")}`);
612813
+ }
612814
+ return sections.join("\n\n");
612815
+ }
612816
+ relevantTelegramSqliteMirrorContext(sessionKey, msg, limit = 12) {
612817
+ const db = this.telegramDb();
612818
+ if (!db) return "";
612819
+ const rows = /* @__PURE__ */ new Map();
612820
+ const addRows = (items) => {
612821
+ for (const row of items) {
612822
+ const key = Number(row.rowid);
612823
+ if (Number.isFinite(key)) rows.set(key, row);
612824
+ }
612825
+ };
612826
+ try {
612827
+ if (msg.fromUserId !== void 0) {
612828
+ addRows(db.prepare(`
612829
+ SELECT rowid, * FROM telegram_messages
612830
+ WHERE session_key = ? AND from_user_id = ?
612831
+ ORDER BY received_at DESC
612832
+ LIMIT ?
612833
+ `).all(sessionKey, msg.fromUserId, limit));
612834
+ }
612835
+ const username = (msg.username || "").replace(/^@/, "").toLowerCase();
612836
+ if (username) {
612837
+ addRows(db.prepare(`
612838
+ SELECT rowid, * FROM telegram_messages
612839
+ WHERE session_key = ? AND lower(username) = ?
612840
+ ORDER BY received_at DESC
612841
+ LIMIT ?
612842
+ `).all(sessionKey, username, Math.max(4, Math.floor(limit / 2))));
612843
+ }
612844
+ const queryTokens = [...telegramMemoryTokens(msg.text)].slice(0, 6);
612845
+ for (const token of queryTokens) {
612846
+ addRows(db.prepare(`
612847
+ SELECT rowid, * FROM telegram_messages
612848
+ WHERE session_key = ? AND text LIKE ?
612849
+ ORDER BY received_at DESC
612850
+ LIMIT 4
612851
+ `).all(sessionKey, `%${token}%`));
612852
+ }
612853
+ } catch {
612854
+ return "";
612855
+ }
612856
+ const selected = [...rows.values()].sort((a2, b) => Number(b.received_at ?? 0) - Number(a2.received_at ?? 0)).slice(0, limit);
612857
+ if (selected.length === 0) return "";
612858
+ const lines = selected.map((row) => {
612859
+ const when = row.received_at ? new Date(Number(row.received_at)).toISOString() : "";
612860
+ const speaker = row.role === "assistant" ? `@${this.state.botUsername || "omnius"}` : row.username ? `@${row.username}` : row.from_user_id ? `user:${row.from_user_id}` : "unknown";
612861
+ const reply = row.reply_to_message_id ? ` reply_to:${row.reply_to_message_id}` : "";
612862
+ return `- ${when} ${speaker}/${row.role || "user"} msg:${row.message_id}${reply}: ${telegramContextJsonString(String(row.text || ""), 260)}`;
612863
+ });
612864
+ return [
612865
+ "### SQLite Telegram Raw Mirror Recall",
612866
+ "Durable local mirror rows for this scoped chat. Use as historical evidence when the rolling context omitted older turns.",
612867
+ lines.join("\n")
612868
+ ].join("\n");
612869
+ }
612870
+ telegramSqliteHistoryForSession(sessionKey, limit = 1e3) {
612871
+ const db = this.telegramDb();
612872
+ if (!db) return [];
612873
+ try {
612874
+ const rows = db.prepare(`
612875
+ SELECT * FROM telegram_messages
612876
+ WHERE session_key = ?
612877
+ ORDER BY received_at DESC
612878
+ LIMIT ?
612879
+ `).all(sessionKey, limit);
612880
+ return rows.reverse().map((row) => ({
612881
+ role: row.role === "assistant" ? "assistant" : "user",
612882
+ text: String(row.text || ""),
612883
+ ts: Number(row.received_at || 0) || void 0,
612884
+ chatId: row.chat_id,
612885
+ speaker: row.role === "assistant" ? `@${this.state.botUsername || "omnius"}` : row.username ? `@${row.username}` : row.from_user_id ? `user:${row.from_user_id}` : "unknown",
612886
+ username: row.username || void 0,
612887
+ firstName: row.first_name || void 0,
612888
+ fromUserId: typeof row.from_user_id === "number" ? row.from_user_id : void 0,
612889
+ messageId: typeof row.message_id === "number" ? row.message_id : void 0,
612890
+ messageThreadId: typeof row.message_thread_id === "number" ? row.message_thread_id : void 0,
612891
+ replyToMessageId: typeof row.reply_to_message_id === "number" ? row.reply_to_message_id : void 0,
612892
+ chatType: row.chat_type,
612893
+ chatTitle: row.chat_title || void 0,
612894
+ mediaSummary: row.media_json ? "media attached in raw Telegram SQLite mirror" : void 0
612895
+ }));
612896
+ } catch {
612897
+ return [];
612898
+ }
612899
+ }
612900
+ searchTelegramSqliteMirrorRows(sessionKey, query, options2 = {}) {
612901
+ const db = this.telegramDb();
612902
+ if (!db) return [];
612903
+ const limit = Math.max(1, Math.min(50, Math.floor(options2.limit ?? 12)));
612904
+ const username = (options2.username || "").replace(/^@/, "").trim().toLowerCase();
612905
+ const tokens = [...telegramMemoryTokens(query)].slice(0, 8);
612906
+ const rows = /* @__PURE__ */ new Map();
612907
+ const addRows = (items) => {
612908
+ for (const row of items) {
612909
+ const key = Number(row.rowid);
612910
+ if (Number.isFinite(key)) rows.set(key, row);
612911
+ }
612912
+ };
612913
+ try {
612914
+ if (options2.userId !== void 0) {
612915
+ addRows(db.prepare(`
612916
+ SELECT rowid, * FROM telegram_messages
612917
+ WHERE session_key = ? AND from_user_id = ?
612918
+ ORDER BY received_at DESC
612919
+ LIMIT ?
612920
+ `).all(sessionKey, options2.userId, limit));
612921
+ }
612922
+ if (username) {
612923
+ addRows(db.prepare(`
612924
+ SELECT rowid, * FROM telegram_messages
612925
+ WHERE session_key = ? AND lower(username) = ?
612926
+ ORDER BY received_at DESC
612927
+ LIMIT ?
612928
+ `).all(sessionKey, username, limit));
612929
+ }
612930
+ if (tokens.length > 0) {
612931
+ const clauses = tokens.map(() => "text LIKE ?").join(" OR ");
612932
+ addRows(db.prepare(`
612933
+ SELECT rowid, * FROM telegram_messages
612934
+ WHERE session_key = ? AND (${clauses})
612935
+ ORDER BY received_at DESC
612936
+ LIMIT ?
612937
+ `).all(sessionKey, ...tokens.map((token) => `%${token}%`), limit));
612938
+ }
612939
+ if (rows.size === 0) {
612940
+ addRows(db.prepare(`
612941
+ SELECT rowid, * FROM telegram_messages
612942
+ WHERE session_key = ?
612943
+ ORDER BY received_at DESC
612944
+ LIMIT ?
612945
+ `).all(sessionKey, Math.min(limit, 12)));
612946
+ }
612947
+ } catch {
612948
+ return [];
612949
+ }
612950
+ return [...rows.values()].sort((a2, b) => Number(b.received_at ?? 0) - Number(a2.received_at ?? 0)).slice(0, limit);
612951
+ }
612952
+ formatTelegramSqliteMirrorRows(rows, maxText = 260) {
612953
+ return rows.map((row) => {
612954
+ const when = row.received_at ? new Date(Number(row.received_at)).toISOString() : "";
612955
+ const speaker = row.role === "assistant" ? `@${this.state.botUsername || "omnius"}` : row.username ? `@${row.username}` : row.from_user_id ? `user:${row.from_user_id}` : "unknown";
612956
+ const reply = row.reply_to_message_id ? ` reply_to:${row.reply_to_message_id}` : "";
612957
+ const media = row.media_json ? " media:attached" : "";
612958
+ return `- ${when} ${speaker}/${row.role || "user"} msg:${row.message_id}${reply}${media}: ${telegramContextJsonString(String(row.text || ""), maxText)}`;
612959
+ }).join("\n");
612960
+ }
612961
+ searchTelegramEpisodeMemory(sessionKey, query, limit = 8) {
612962
+ if (!this.repoRoot || !query.trim()) return [];
612963
+ const paths = omniusMemoryDbPaths(this.repoRoot);
612964
+ if (!existsSync112(paths.episodes)) return [];
612965
+ const graph = new TemporalGraph(paths.knowledge);
612966
+ const store2 = new EpisodeStore(paths.episodes, graph);
612967
+ try {
612968
+ return store2.searchWithPPR(
612969
+ {
612970
+ sessionId: sessionKey,
612971
+ query,
612972
+ limit: Math.max(1, Math.min(20, Math.floor(limit))),
612973
+ metadataFilter: { sourceSurface: "telegram" }
612974
+ },
612975
+ { lexicalWeight: 1.25, embeddingWeight: 0 }
612976
+ );
612977
+ } catch {
612978
+ return [];
612979
+ } finally {
612980
+ store2.close();
612981
+ graph.close();
612982
+ }
612983
+ }
612984
+ formatTelegramEpisodeMemoryResults(episodes, maxText = 320) {
612985
+ return episodes.map((episode) => {
612986
+ const meta = episode.metadata;
612987
+ const telegram = meta?.telegram && typeof meta.telegram === "object" ? meta.telegram : {};
612988
+ const when = episode.timestamp ? new Date(episode.timestamp).toISOString() : "";
612989
+ const speaker = telegram.speaker || telegram.username || "unknown";
612990
+ const messageId = telegram.messageId ? ` msg:${telegram.messageId}` : "";
612991
+ const mode = telegram.mode ? `/${telegram.mode}` : "";
612992
+ 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;
612993
+ return `- ${when} ${speaker}${mode}${messageId}: ${telegramContextJsonString(text, maxText)}`;
612994
+ }).join("\n");
612995
+ }
611958
612996
  buildTelegramConversationContextStream(sessionKey, msg, maxRecent = TELEGRAM_CONTEXT_RECENT_DEFAULT) {
611959
612997
  this.ensureTelegramConversationLoaded(sessionKey);
611960
612998
  const history = this.chatHistory.get(sessionKey) ?? [];
@@ -611993,6 +613031,22 @@ ${mediaContext}` : ""
611993
613031
  sections.push(`### Participants And Relationship Signals
611994
613032
  ${participantLines.join("\n")}`);
611995
613033
  }
613034
+ const associativeContext = this.relevantTelegramAssociativeMemoryContext(
613035
+ sessionKey,
613036
+ msg,
613037
+ isGroup ? 14 : 8
613038
+ );
613039
+ if (associativeContext) {
613040
+ sections.push(associativeContext);
613041
+ }
613042
+ const sqliteMirrorContext = this.relevantTelegramSqliteMirrorContext(
613043
+ sessionKey,
613044
+ msg,
613045
+ isGroup ? 14 : 8
613046
+ );
613047
+ if (sqliteMirrorContext) {
613048
+ sections.push(sqliteMirrorContext);
613049
+ }
611996
613050
  const memoryCards = this.relevantTelegramMemoryCards(sessionKey, msg, isGroup ? 10 : 6);
611997
613051
  if (memoryCards.length > 0) {
611998
613052
  const cardLines = memoryCards.map(({ card, score }) => {
@@ -612176,7 +613230,8 @@ ${lines.join("\n")}`);
612176
613230
  ``,
612177
613231
  `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
613232
  `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.`,
613233
+ `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.`,
613234
+ `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
613235
  `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
613236
  `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
613237
  forcedLine,
@@ -612601,6 +613656,13 @@ ${TELEGRAM_PUBLIC_ORCHESTRATOR_CONTRACT}`);
612601
613656
  agent.aborted = true;
612602
613657
  if (agent.typingInterval) clearInterval(agent.typingInterval);
612603
613658
  }
613659
+ if (this.telegramSqliteDb && this.telegramSqliteDb !== false) {
613660
+ try {
613661
+ this.telegramSqliteDb.close();
613662
+ } catch {
613663
+ }
613664
+ this.telegramSqliteDb = null;
613665
+ }
612604
613666
  this.subAgents.clear();
612605
613667
  this.activeChatViews.clear();
612606
613668
  this.refreshActiveTelegramInteractionCount();
@@ -613529,7 +614591,7 @@ ${currentTelegramPrompt}`;
613529
614591
  const toolHint = [
613530
614592
  "You have access to isolated per-chat memory (memory_write, memory_read, memory_search) scoped to this conversation.",
613531
614593
  "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.",
614594
+ "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
614595
  "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
614596
  formatIdentityMemoryContext(chatLabel || "Telegram private chat"),
613535
614597
  reminderToolContract,
@@ -613695,7 +614757,7 @@ ${result.llmContent ?? result.output}` };
613695
614757
  if (tool.name === "memory_search") {
613696
614758
  return {
613697
614759
  ...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.",
614760
+ 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
614761
  parameters: (() => {
613700
614762
  const base3 = tool.parameters ?? {};
613701
614763
  const props = base3["properties"] && typeof base3["properties"] === "object" && !Array.isArray(base3["properties"]) ? base3["properties"] : {};
@@ -613713,6 +614775,8 @@ ${result.llmContent ?? result.output}` };
613713
614775
  execute: async (args) => {
613714
614776
  const query = String(args["query"] || "").trim();
613715
614777
  const maxResults = typeof args["max_results"] === "number" && Number.isFinite(args["max_results"]) ? Math.max(1, Math.min(20, Math.floor(args["max_results"]))) : 8;
614778
+ if (!query) return { success: true, output: "Search query is required." };
614779
+ this.ensureTelegramConversationLoaded(msgSessionKey);
613716
614780
  const currentGroupId = chatId === void 0 ? "" : String(chatId);
613717
614781
  const requestedGroupId = String(args["group_id"] ?? args["chat_id"] ?? "").trim();
613718
614782
  if (requestedGroupId && currentGroupId && requestedGroupId !== currentGroupId) {
@@ -613740,32 +614804,70 @@ ${result.llmContent ?? result.output}` };
613740
614804
  if (effectiveUsername && haystack.includes(`@${effectiveUsername}`)) return true;
613741
614805
  return false;
613742
614806
  });
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
614807
  const queryTokens = telegramMemoryTokens(query);
613748
- const results = cards.map((card) => ({
614808
+ const cardResults = cards.map((card) => ({
613749
614809
  card,
613750
614810
  score: telegramMemorySimilarity(
613751
614811
  queryTokens,
613752
614812
  telegramMemoryTokens([card.title, card.tags.join(" "), card.speakers.join(" "), card.notes.join(" ")].join(" "))
613753
614813
  )
613754
614814
  })).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 }) => {
614815
+ const cardLines = cardResults.map(({ card, score }) => {
613759
614816
  const notes2 = card.notes.slice(-4).map((note) => ` - ${truncateTelegramContextLine(note, 220)}`).join("\n");
613760
614817
  const tags = card.tags.length ? ` tags:${card.tags.slice(0, 8).join(",")}` : "";
613761
614818
  const users = Array.isArray(card.userIds) && card.userIds.length ? ` users:${card.userIds.slice(0, 6).join(",")}` : "";
613762
614819
  return `[${card.id}] ${card.title} (relevance ${score.toFixed(2)}${users}${tags})
613763
614820
  ${notes2}`;
613764
614821
  });
614822
+ const memory = this.chatAssociativeMemory.get(msgSessionKey);
614823
+ const associativeFacts = memory ? [...memory.facts, ...memory.relationships].filter((fact) => {
614824
+ if (!wantsUserScope) return true;
614825
+ if (effectiveUserId !== void 0 && fact.userIds.includes(effectiveUserId)) return true;
614826
+ return !!effectiveUsername && fact.usernames.includes(effectiveUsername);
614827
+ }).map((fact) => ({
614828
+ fact,
614829
+ score: telegramMemorySimilarity(
614830
+ queryTokens,
614831
+ telegramMemoryTokens([fact.text, fact.tags.join(" "), fact.speakers.join(" ")].join(" "))
614832
+ ) + (effectiveUserId !== void 0 && fact.userIds.includes(effectiveUserId) ? 0.3 : 0) + (effectiveUsername && fact.usernames.includes(effectiveUsername) ? 0.3 : 0)
614833
+ })).filter((item) => item.score > 0).sort((a2, b) => b.score - a2.score || b.fact.updatedAt - a2.fact.updatedAt).slice(0, maxResults) : [];
614834
+ const rawRows = this.searchTelegramSqliteMirrorRows(msgSessionKey, query, {
614835
+ limit: maxResults,
614836
+ userId: effectiveUserId,
614837
+ username: effectiveUsername
614838
+ });
614839
+ const episodeResults = this.searchTelegramEpisodeMemory(msgSessionKey, query, maxResults);
613765
614840
  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")}` };
614841
+ const sections = [];
614842
+ if (cardLines.length > 0) {
614843
+ sections.push(`### Scoped Memory Cards
614844
+ ${cardLines.join("\n\n")}`);
614845
+ }
614846
+ if (associativeFacts.length > 0) {
614847
+ const lines = associativeFacts.map(
614848
+ ({ fact, score }) => `- (${score.toFixed(2)}) ${telegramContextJsonString(fact.text, 300)}${fact.messageIds.length ? ` [messages:${fact.messageIds.slice(-6).join(",")}]` : ""}`
614849
+ );
614850
+ sections.push(`### Durable Associative Facts
614851
+ ${lines.join("\n")}`);
614852
+ }
614853
+ if (episodeResults.length > 0) {
614854
+ sections.push(`### Episode/Knowledge Graph Recall
614855
+ ${this.formatTelegramEpisodeMemoryResults(episodeResults)}`);
614856
+ }
614857
+ if (rawRows.length > 0) {
614858
+ sections.push(`### Raw SQLite Message Mirror
614859
+ ${this.formatTelegramSqliteMirrorRows(rawRows)}`);
614860
+ }
614861
+ if (sections.length === 0) {
614862
+ return { success: true, output: `No scoped Telegram memories matched "${query}" in ${scopeLabel}.` };
614863
+ }
614864
+ const output = [
614865
+ `Scoped Telegram memory search for "${query}" in ${scopeLabel}.`,
614866
+ "Results are scoped to this Telegram chat and may include raw message evidence, graph episodes, associative facts, and memory cards.",
614867
+ "",
614868
+ sections.join("\n\n")
614869
+ ].join("\n");
614870
+ return { success: true, output, llmContent: output };
613769
614871
  }
613770
614872
  };
613771
614873
  }
@@ -616320,6 +617422,7 @@ ${caption}\r
616320
617422
  }
616321
617423
  const msg = normalizeTelegramUpdate(update2);
616322
617424
  if (!msg) continue;
617425
+ this.persistTelegramRawMessage(update2, msg);
616323
617426
  const isAdmin = this.adminUserId ? String(msg.fromUserId) === this.adminUserId || msg.username === this.adminUserId : false;
616324
617427
  if (this.adminUserId && !this.agentConfig) {
616325
617428
  if (!isAdmin) continue;
@@ -616665,8 +617768,8 @@ function appendCheckin(sessionId, steering) {
616665
617768
  mkdirSync66(sessionsDir(), { recursive: true });
616666
617769
  const fp = checkinPath(sessionId);
616667
617770
  const entry = JSON.stringify({ ts: Date.now(), steering }) + "\n";
616668
- const { appendFileSync: appendFileSync9 } = __require("node:fs");
616669
- appendFileSync9(fp, entry, "utf-8");
617771
+ const { appendFileSync: appendFileSync12 } = __require("node:fs");
617772
+ appendFileSync12(fp, entry, "utf-8");
616670
617773
  } catch {
616671
617774
  }
616672
617775
  }
@@ -618865,7 +619968,7 @@ __export(audit_log_exports, {
618865
619968
  recordAudit: () => recordAudit,
618866
619969
  sanitizeBody: () => sanitizeBody
618867
619970
  });
618868
- import { mkdirSync as mkdirSync69, appendFileSync as appendFileSync7, readFileSync as readFileSync96, existsSync as existsSync116 } from "node:fs";
619971
+ import { mkdirSync as mkdirSync69, appendFileSync as appendFileSync10, readFileSync as readFileSync96, existsSync as existsSync116 } from "node:fs";
618869
619972
  import { join as join131 } from "node:path";
618870
619973
  function initAuditLog(omniusDir) {
618871
619974
  auditDir = join131(omniusDir, "audit");
@@ -618880,7 +619983,7 @@ function recordAudit(record) {
618880
619983
  if (!initialized) return;
618881
619984
  try {
618882
619985
  const line = JSON.stringify(record) + "\n";
618883
- appendFileSync7(auditFile, line, "utf-8");
619986
+ appendFileSync10(auditFile, line, "utf-8");
618884
619987
  } catch {
618885
619988
  }
618886
619989
  }
@@ -641179,7 +642282,7 @@ import { fileURLToPath as fileURLToPath18 } from "node:url";
641179
642282
  import {
641180
642283
  readFileSync as readFileSync105,
641181
642284
  writeFileSync as writeFileSync70,
641182
- appendFileSync as appendFileSync8,
642285
+ appendFileSync as appendFileSync11,
641183
642286
  rmSync as rmSync7,
641184
642287
  readdirSync as readdirSync46,
641185
642288
  mkdirSync as mkdirSync78
@@ -645673,7 +646776,7 @@ This is an independent background session started from /background.`
645673
646776
  if (!line.trim()) return;
645674
646777
  try {
645675
646778
  mkdirSync78(HISTORY_DIR, { recursive: true });
645676
- appendFileSync8(HISTORY_FILE, line + "\n", "utf8");
646779
+ appendFileSync11(HISTORY_FILE, line + "\n", "utf8");
645677
646780
  if (Math.random() < 0.02) {
645678
646781
  const all2 = readFileSync105(HISTORY_FILE, "utf8").trim().split("\n");
645679
646782
  if (all2.length > MAX_HISTORY_LINES) {
@@ -651460,12 +652563,12 @@ function crashLog(label, err) {
651460
652563
  const logLine = `[${timestamp}] ${label}: ${msg}
651461
652564
  `;
651462
652565
  try {
651463
- const { appendFileSync: appendFileSync9, mkdirSync: mkdirSync81 } = __require("node:fs");
652566
+ const { appendFileSync: appendFileSync12, mkdirSync: mkdirSync81 } = __require("node:fs");
651464
652567
  const { join: join148 } = __require("node:path");
651465
652568
  const { homedir: homedir52 } = __require("node:os");
651466
652569
  const logDir = join148(homedir52(), ".omnius");
651467
652570
  mkdirSync81(logDir, { recursive: true });
651468
- appendFileSync9(join148(logDir, "crash.log"), logLine);
652571
+ appendFileSync12(join148(logDir, "crash.log"), logLine);
651469
652572
  } catch {
651470
652573
  }
651471
652574
  try {