omnius 1.0.65 → 1.0.66

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
@@ -609476,6 +609476,147 @@ function parseTelegramInteractionDecision(text, forcedRoute, options2 = {}) {
609476
609476
  return null;
609477
609477
  }
609478
609478
  }
609479
+ function parseTelegramTimeRangeQuery(query, now = /* @__PURE__ */ new Date()) {
609480
+ const original = String(query || "");
609481
+ const lower = original.toLowerCase();
609482
+ const nowMs = now.getTime();
609483
+ const dayMs = 864e5;
609484
+ const hourMs = 36e5;
609485
+ const minMs = 6e4;
609486
+ const weekMs = 7 * dayMs;
609487
+ const startOfDay = (d2) => {
609488
+ const x = new Date(d2);
609489
+ x.setHours(0, 0, 0, 0);
609490
+ return x;
609491
+ };
609492
+ const endOfDay = (d2) => {
609493
+ const x = new Date(d2);
609494
+ x.setHours(23, 59, 59, 999);
609495
+ return x;
609496
+ };
609497
+ let since;
609498
+ let until;
609499
+ let bucket;
609500
+ let label = "";
609501
+ let residual = original;
609502
+ const strip = (re) => {
609503
+ residual = residual.replace(re, " ").replace(/\s+/g, " ").trim();
609504
+ };
609505
+ if (/\b(oldest|earliest|first)\b/.test(lower)) {
609506
+ bucket = "earliest";
609507
+ label = label || "earliest";
609508
+ strip(/\b(?:the\s+)?(?:oldest|earliest|first)(?:\s+(?:memory|entry|turn|message|memories))?\b/gi);
609509
+ } else if (/\b(newest|latest|most\s+recent|last\s+memory|last\s+turn|last\s+message)\b/.test(lower)) {
609510
+ bucket = "latest";
609511
+ label = label || "latest";
609512
+ strip(/\b(?:the\s+)?(?:newest|latest|most\s+recent|last\s+(?:memory|turn|message))\b/gi);
609513
+ }
609514
+ if (/\byesterday\b/.test(lower)) {
609515
+ const y = new Date(now);
609516
+ y.setDate(y.getDate() - 1);
609517
+ since = startOfDay(y).getTime();
609518
+ until = endOfDay(y).getTime();
609519
+ label = label ? `${label}, yesterday` : "yesterday";
609520
+ strip(/\byesterday\b/gi);
609521
+ } else if (/\btoday\b/.test(lower)) {
609522
+ since = startOfDay(now).getTime();
609523
+ until = endOfDay(now).getTime();
609524
+ label = label ? `${label}, today` : "today";
609525
+ strip(/\btoday\b/gi);
609526
+ }
609527
+ const partMatch = lower.match(/\bthis\s+(morning|afternoon|evening|night|tonight)\b/);
609528
+ if (partMatch) {
609529
+ const today0 = startOfDay(now).getTime();
609530
+ if (partMatch[1] === "morning") {
609531
+ since = today0;
609532
+ until = today0 + 12 * hourMs;
609533
+ } else if (partMatch[1] === "afternoon") {
609534
+ since = today0 + 12 * hourMs;
609535
+ until = today0 + 18 * hourMs;
609536
+ } else if (partMatch[1] === "evening") {
609537
+ since = today0 + 17 * hourMs;
609538
+ until = today0 + 22 * hourMs;
609539
+ } else {
609540
+ since = today0 + 20 * hourMs;
609541
+ until = endOfDay(now).getTime();
609542
+ }
609543
+ label = label ? `${label}, this ${partMatch[1]}` : `this ${partMatch[1]}`;
609544
+ strip(/\bthis\s+(?:morning|afternoon|evening|night|tonight)\b/gi);
609545
+ }
609546
+ const nMatch = lower.match(/\b(?:last|past|in\s+the\s+last|in\s+the\s+past)\s+(\d+)?\s*(minute|min|hour|hr|day|week|wk)s?\b/);
609547
+ if (nMatch) {
609548
+ const n2 = nMatch[1] ? parseInt(nMatch[1], 10) : 1;
609549
+ const u = nMatch[2];
609550
+ const stepMs = u.startsWith("min") ? minMs : u.startsWith("h") ? hourMs : u.startsWith("d") ? dayMs : weekMs;
609551
+ since = nowMs - n2 * stepMs;
609552
+ until = nowMs;
609553
+ label = label ? `${label}, last ${n2} ${u}${n2 === 1 ? "" : "s"}` : `last ${n2} ${u}${n2 === 1 ? "" : "s"}`;
609554
+ strip(/\b(?:last|past|in\s+the\s+last|in\s+the\s+past)\s+\d*\s*(?:minute|min|hour|hr|day|week|wk)s?\b/gi);
609555
+ }
609556
+ const agoMatch = lower.match(/\b(\d+)\s+(minute|min|hour|hr|day|week|wk)s?\s+ago\b/);
609557
+ if (agoMatch) {
609558
+ const n2 = parseInt(agoMatch[1], 10);
609559
+ const u = agoMatch[2];
609560
+ const stepMs = u.startsWith("min") ? minMs : u.startsWith("h") ? hourMs : u.startsWith("d") ? dayMs : weekMs;
609561
+ const center = nowMs - n2 * stepMs;
609562
+ if (u.startsWith("d")) {
609563
+ const at = new Date(center);
609564
+ since = startOfDay(at).getTime();
609565
+ until = endOfDay(at).getTime();
609566
+ } else {
609567
+ since = center - stepMs;
609568
+ until = center + stepMs;
609569
+ }
609570
+ label = label ? `${label}, ${n2} ${u}${n2 === 1 ? "" : "s"} ago` : `${n2} ${u}${n2 === 1 ? "" : "s"} ago`;
609571
+ strip(/\b\d+\s+(?:minute|min|hour|hr|day|week|wk)s?\s+ago\b/gi);
609572
+ }
609573
+ if (/\bearlier\s+today\b/.test(lower)) {
609574
+ const today0 = startOfDay(now).getTime();
609575
+ since = today0;
609576
+ until = nowMs - 30 * minMs;
609577
+ label = label ? `${label}, earlier today` : "earlier today";
609578
+ strip(/\bearlier\s+today\b/gi);
609579
+ } else if (/\bearlier\b/.test(lower) && since === void 0 && bucket === void 0) {
609580
+ since = nowMs - dayMs;
609581
+ until = nowMs - 30 * minMs;
609582
+ label = label ? `${label}, earlier` : "earlier";
609583
+ strip(/\bearlier\b/gi);
609584
+ }
609585
+ const sinceIsoMatch = original.match(/\bsince\s+(\d{4}-\d{2}-\d{2}(?:[T\s]\d{2}:\d{2}(?::\d{2})?)?)\b/i);
609586
+ if (sinceIsoMatch) {
609587
+ const t2 = Date.parse(sinceIsoMatch[1]);
609588
+ if (Number.isFinite(t2)) {
609589
+ since = t2;
609590
+ until = nowMs;
609591
+ label = label ? `${label}, since ${sinceIsoMatch[1]}` : `since ${sinceIsoMatch[1]}`;
609592
+ strip(new RegExp(`\\bsince\\s+${sinceIsoMatch[1].replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "gi"));
609593
+ }
609594
+ }
609595
+ const onIsoMatch = original.match(/\bon\s+(\d{4}-\d{2}-\d{2})\b/i);
609596
+ if (onIsoMatch) {
609597
+ const t2 = Date.parse(onIsoMatch[1]);
609598
+ if (Number.isFinite(t2)) {
609599
+ const d2 = new Date(t2);
609600
+ since = startOfDay(d2).getTime();
609601
+ until = endOfDay(d2).getTime();
609602
+ label = label ? `${label}, on ${onIsoMatch[1]}` : `on ${onIsoMatch[1]}`;
609603
+ strip(new RegExp(`\\bon\\s+${onIsoMatch[1]}\\b`, "gi"));
609604
+ }
609605
+ }
609606
+ const betweenMatch = original.match(/\bbetween\s+(\d{4}-\d{2}-\d{2})\s+and\s+(\d{4}-\d{2}-\d{2})\b/i);
609607
+ if (betweenMatch) {
609608
+ const a2 = Date.parse(betweenMatch[1]);
609609
+ const b = Date.parse(betweenMatch[2]);
609610
+ if (Number.isFinite(a2) && Number.isFinite(b)) {
609611
+ since = Math.min(a2, b);
609612
+ until = Math.max(a2, b) + dayMs;
609613
+ label = label ? `${label}, between ${betweenMatch[1]} and ${betweenMatch[2]}` : `between ${betweenMatch[1]} and ${betweenMatch[2]}`;
609614
+ strip(/\bbetween\s+\d{4}-\d{2}-\d{2}\s+and\s+\d{4}-\d{2}-\d{2}\b/gi);
609615
+ }
609616
+ }
609617
+ if (since === void 0 && until === void 0 && bucket === void 0) return null;
609618
+ return { since, until, bucket, label: label || "time-range", residualQuery: residual };
609619
+ }
609479
609620
  function parseTelegramReflectionFollowupDecision(text) {
609480
609621
  const cleaned = stripTelegramHiddenThinking(text).replace(/```(?:json)?/gi, "").replace(/```/g, "").trim();
609481
609622
  const jsonText = cleaned.startsWith("{") ? cleaned : cleaned.match(/\{[\s\S]*\}/)?.[0] ?? "";
@@ -613332,11 +613473,116 @@ ${lines.join("\n")}`);
613332
613473
  lines.push(` - ${topic}`);
613333
613474
  }
613334
613475
  }
613476
+ const sourceHistory = this.chatHistory.get(sessionKey) ?? [];
613477
+ const sortedByTs = [...sourceHistory].filter((e2) => typeof e2.ts === "number" && Number.isFinite(e2.ts)).sort((a2, b) => (a2.ts ?? 0) - (b.ts ?? 0));
613478
+ const fmtHistoryAnchor = (entry) => {
613479
+ const when = entry.ts ? new Date(entry.ts).toISOString() : "(unknown ts)";
613480
+ const speaker = telegramHistorySpeaker(entry);
613481
+ const mode = entry.mode ? `/${entry.mode}` : "";
613482
+ const messageId = entry.messageId ? ` msg:${entry.messageId}` : "";
613483
+ return `${when} ${speaker}${mode}${messageId}: ${telegramContextJsonString(String(entry.text || ""), 320)}`;
613484
+ };
613485
+ if (sortedByTs.length > 0) {
613486
+ lines.push("");
613487
+ lines.push("Chronological anchors — Telegram conversation history (ground truth for 'oldest/newest memory' questions):");
613488
+ lines.push(` EARLIEST turn: ${fmtHistoryAnchor(sortedByTs[0])}`);
613489
+ if (sortedByTs.length > 1) {
613490
+ lines.push(` LATEST turn: ${fmtHistoryAnchor(sortedByTs[sortedByTs.length - 1])}`);
613491
+ }
613492
+ if (sortedByTs.length > 2) {
613493
+ lines.push(` 2nd earliest: ${fmtHistoryAnchor(sortedByTs[1])}`);
613494
+ }
613495
+ if (sortedByTs.length > 3) {
613496
+ lines.push(` 3rd earliest: ${fmtHistoryAnchor(sortedByTs[2])}`);
613497
+ }
613498
+ }
613499
+ if (this.repoRoot) {
613500
+ try {
613501
+ const paths = omniusMemoryDbPaths(this.repoRoot);
613502
+ if (existsSync113(paths.episodes)) {
613503
+ const graph = new TemporalGraph(paths.knowledge);
613504
+ const store2 = new EpisodeStore(paths.episodes, graph);
613505
+ try {
613506
+ const dbAny = store2.db;
613507
+ if (dbAny && typeof dbAny.prepare === "function") {
613508
+ const earliest = dbAny.prepare(
613509
+ "SELECT timestamp, modality, tool_name, content, gist FROM episodes WHERE session_id = ? ORDER BY timestamp ASC LIMIT 1"
613510
+ ).get(sessionKey);
613511
+ const latest = dbAny.prepare(
613512
+ "SELECT timestamp, modality, tool_name, content, gist FROM episodes WHERE session_id = ? ORDER BY timestamp DESC LIMIT 1"
613513
+ ).get(sessionKey);
613514
+ if (earliest || latest) {
613515
+ lines.push("");
613516
+ lines.push("Chronological anchors — episodes.db (durable, may reach further back than rolling history):");
613517
+ const fmtEp = (row) => {
613518
+ const when = row.timestamp ? new Date(row.timestamp).toISOString() : "(unknown ts)";
613519
+ const tag = `[${row.modality || "?"}${row.tool_name ? ":" + row.tool_name : ""}]`;
613520
+ const text = (row.gist || row.content || "").split("\n").filter((ln) => !/^(Telegram|session:|chat:|message_id:|thread_id:|speaker:|mode:)/i.test(ln.trim())).join(" ").replace(/\s+/g, " ").trim();
613521
+ return `${when} ${tag} ${telegramContextJsonString(text, 320)}`;
613522
+ };
613523
+ if (earliest) lines.push(` EARLIEST episode: ${fmtEp(earliest)}`);
613524
+ if (latest && (!earliest || earliest.timestamp !== latest.timestamp)) lines.push(` LATEST episode: ${fmtEp(latest)}`);
613525
+ }
613526
+ }
613527
+ } finally {
613528
+ try {
613529
+ store2.close();
613530
+ } catch {
613531
+ }
613532
+ try {
613533
+ graph.close();
613534
+ } catch {
613535
+ }
613536
+ }
613537
+ }
613538
+ } catch {
613539
+ }
613540
+ }
613541
+ if (this.repoRoot && chatId !== void 0 && topicFiles.length > 0) {
613542
+ try {
613543
+ const memDir = resolve43(this.repoRoot, ".omnius", "memory");
613544
+ const prefix = this.telegramScopedMemoryPrefix(chatId);
613545
+ let earliestEntry = null;
613546
+ let latestEntry = null;
613547
+ for (const topic of topicFiles) {
613548
+ const file = join127(memDir, `${prefix}${topic}.json`);
613549
+ if (!existsSync113(file)) continue;
613550
+ let parsed;
613551
+ try {
613552
+ parsed = JSON.parse(readFileSync92(file, "utf8"));
613553
+ } catch {
613554
+ continue;
613555
+ }
613556
+ for (const [key, entry] of Object.entries(parsed)) {
613557
+ if (!entry || typeof entry !== "object") continue;
613558
+ const rawTs = entry.timestamp;
613559
+ const ts = typeof rawTs === "string" ? Date.parse(rawTs) : typeof rawTs === "number" ? rawTs : NaN;
613560
+ if (!Number.isFinite(ts)) continue;
613561
+ const value2 = String(entry.value ?? "");
613562
+ if (!earliestEntry || ts < earliestEntry.ts) earliestEntry = { topic, key, value: value2, ts };
613563
+ if (!latestEntry || ts > latestEntry.ts) latestEntry = { topic, key, value: value2, ts };
613564
+ }
613565
+ }
613566
+ if (earliestEntry || latestEntry) {
613567
+ lines.push("");
613568
+ lines.push("Chronological anchors — memory_write entries (most-trusted, agent-asserted):");
613569
+ const fmtMem = (e2) => {
613570
+ const when = new Date(e2.ts).toISOString();
613571
+ return `${when} topic="${e2.topic}" key="${e2.key}" → ${telegramContextJsonString(e2.value, 320)}`;
613572
+ };
613573
+ if (earliestEntry) lines.push(` EARLIEST memory_write: ${fmtMem(earliestEntry)}`);
613574
+ if (latestEntry && (!earliestEntry || earliestEntry.ts !== latestEntry.ts)) lines.push(` LATEST memory_write: ${fmtMem(latestEntry)}`);
613575
+ }
613576
+ } catch {
613577
+ }
613578
+ }
613335
613579
  lines.push("");
613336
613580
  lines.push("RULES:");
613337
613581
  lines.push(" 1. NEVER tell the user 'memory is empty' or 'nothing has been stored' for this chat without first calling memory_search and memory_read on a relevant topic from the list above.");
613338
613582
  lines.push(" 2. If the structured sections (cards/facts/sqlite/episodes) above did not surface what the user asked about, that is a SCORING miss, not absence. Call memory_search with broader tokens or pick a topic above with memory_read.");
613339
613583
  lines.push(" 3. The rolling-history block is base context; the cards/facts/episodes are retrieval-augmented. Treat them as the same memory, surfaced different ways.");
613584
+ lines.push(" 4. For 'what is your oldest/earliest memory' or 'most recent memory' questions: answer DIRECTLY from the 'Chronological anchors' lines above. Quote the timestamp and content. Do NOT call tools first and do NOT report 'empty'.");
613585
+ lines.push(" 5. memory_search accepts NATURAL-LANGUAGE TIME phrases inside the `query` argument or explicit `since`/`until`/`bucket` args. Examples: query='what did manitcor say yesterday', query='last 3 hours', query='earlier today', query='2 days ago', query='since 2026-05-15', query='between 2026-05-15 and 2026-05-16', query='oldest memory about github', query='most recent flux discussion'. Use these for chronological/'how far back' style queries instead of guessing — the tool parses the phrase, filters by time, and returns the right window.");
613340
613586
  return lines.join("\n");
613341
613587
  }
613342
613588
  buildTelegramConversationContextStream(sessionKey, msg, maxRecent = TELEGRAM_CONTEXT_RECENT_DEFAULT) {
@@ -615180,7 +615426,7 @@ ${result.llmContent ?? result.output}` };
615180
615426
  if (tool.name === "memory_search") {
615181
615427
  return {
615182
615428
  ...tool,
615183
- 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.",
615429
+ 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; also supports natural-language time phrases inside the query string ('yesterday', 'last 3 hours', '2 days ago', 'earlier today', 'since 2026-05-15', 'between A and B', 'oldest', 'most recent') OR explicit since/until/bucket arguments. Never crosses into admin/private/global memory.",
615184
615430
  parameters: (() => {
615185
615431
  const base3 = tool.parameters ?? {};
615186
615432
  const props = base3["properties"] && typeof base3["properties"] === "object" && !Array.isArray(base3["properties"]) ? base3["properties"] : {};
@@ -615191,15 +615437,57 @@ ${result.llmContent ?? result.output}` };
615191
615437
  scope: { type: "string", enum: ["group", "current_chat", "user"], description: "Search scope inside the current Telegram chat. scope=user defaults to the current sender when no user_id/username is supplied." },
615192
615438
  user_id: { type: "number", description: "Optional Telegram user id to search within the current group only." },
615193
615439
  username: { type: "string", description: "Optional Telegram username to search within the current group only." },
615194
- group_id: { type: "string", description: "Optional Telegram group/chat id. Must match the current chat id; other groups are not accessible here." }
615440
+ group_id: { type: "string", description: "Optional Telegram group/chat id. Must match the current chat id; other groups are not accessible here." },
615441
+ since: { type: "string", description: "Optional lower-bound timestamp (ISO 8601 or epoch ms). Combine with `until` for an explicit window. If omitted, natural-language phrases inside `query` are auto-parsed (e.g. 'yesterday', '3 days ago')." },
615442
+ until: { type: "string", description: "Optional upper-bound timestamp (ISO 8601 or epoch ms)." },
615443
+ bucket: { type: "string", enum: ["earliest", "latest"], description: "Chronological ordering hint when there is no specific window. 'earliest' returns the oldest entries on record; 'latest' returns the newest." }
615195
615444
  }
615196
615445
  };
615197
615446
  })(),
615198
615447
  execute: async (args) => {
615199
- const query = String(args["query"] || "").trim();
615448
+ const rawQuery = String(args["query"] || "").trim();
615200
615449
  const maxResults = typeof args["max_results"] === "number" && Number.isFinite(args["max_results"]) ? Math.max(1, Math.min(20, Math.floor(args["max_results"]))) : 8;
615201
- if (!query) return { success: true, output: "Search query is required." };
615450
+ if (!rawQuery) return { success: true, output: "Search query is required." };
615202
615451
  this.ensureTelegramConversationLoaded(msgSessionKey);
615452
+ const parseTimeArg = (v) => {
615453
+ if (v === void 0 || v === null || v === "") return void 0;
615454
+ if (typeof v === "number" && Number.isFinite(v)) return Math.trunc(v);
615455
+ if (typeof v === "string") {
615456
+ if (/^\d+$/.test(v.trim())) return Number(v.trim());
615457
+ const t2 = Date.parse(v);
615458
+ if (Number.isFinite(t2)) return t2;
615459
+ }
615460
+ return void 0;
615461
+ };
615462
+ let since = parseTimeArg(args["since"]);
615463
+ let until = parseTimeArg(args["until"]);
615464
+ let bucket = args["bucket"] === "earliest" || args["bucket"] === "latest" ? args["bucket"] : void 0;
615465
+ let timeLabel = "";
615466
+ let effectiveQuery = rawQuery;
615467
+ if (since === void 0 && until === void 0 && bucket === void 0) {
615468
+ const parsed = parseTelegramTimeRangeQuery(rawQuery);
615469
+ if (parsed) {
615470
+ since = parsed.since;
615471
+ until = parsed.until;
615472
+ bucket = parsed.bucket;
615473
+ timeLabel = parsed.label;
615474
+ effectiveQuery = parsed.residualQuery || rawQuery;
615475
+ }
615476
+ } else if (since !== void 0 || until !== void 0 || bucket !== void 0) {
615477
+ timeLabel = [
615478
+ since !== void 0 ? `since=${new Date(since).toISOString()}` : "",
615479
+ until !== void 0 ? `until=${new Date(until).toISOString()}` : "",
615480
+ bucket ? `bucket=${bucket}` : ""
615481
+ ].filter(Boolean).join(", ");
615482
+ }
615483
+ const inWindow = (ts) => {
615484
+ if (since === void 0 && until === void 0) return true;
615485
+ if (ts == null || !Number.isFinite(ts)) return false;
615486
+ if (since !== void 0 && ts < since) return false;
615487
+ if (until !== void 0 && ts > until) return false;
615488
+ return true;
615489
+ };
615490
+ const query = effectiveQuery.trim();
615203
615491
  const currentGroupId = chatId === void 0 ? "" : String(chatId);
615204
615492
  const requestedGroupId = String(args["group_id"] ?? args["chat_id"] ?? "").trim();
615205
615493
  if (requestedGroupId && currentGroupId && requestedGroupId !== currentGroupId) {
@@ -615228,13 +615516,17 @@ ${result.llmContent ?? result.output}` };
615228
615516
  return false;
615229
615517
  });
615230
615518
  const queryTokens = telegramMemoryTokens(query);
615231
- const cardResults = cards.map((card) => ({
615519
+ const cardResults = cards.filter((card) => inWindow(card.updatedAt ?? card.createdAt)).map((card) => ({
615232
615520
  card,
615233
615521
  score: telegramMemorySimilarity(
615234
615522
  queryTokens,
615235
615523
  telegramMemoryTokens([card.title, card.tags.join(" "), card.speakers.join(" "), card.notes.join(" ")].join(" "))
615236
615524
  )
615237
- })).filter((item) => item.score > 0).sort((a2, b) => b.score - a2.score || b.card.updatedAt - a2.card.updatedAt).slice(0, maxResults);
615525
+ })).filter((item) => item.score > 0 || since !== void 0 || until !== void 0 || bucket !== void 0).sort((a2, b) => {
615526
+ if (bucket === "earliest") return (a2.card.updatedAt ?? 0) - (b.card.updatedAt ?? 0);
615527
+ if (bucket === "latest") return (b.card.updatedAt ?? 0) - (a2.card.updatedAt ?? 0);
615528
+ return b.score - a2.score || b.card.updatedAt - a2.card.updatedAt;
615529
+ }).slice(0, maxResults);
615238
615530
  const cardLines = cardResults.map(({ card, score }) => {
615239
615531
  const notes2 = card.notes.slice(-4).map((note) => ` - ${truncateTelegramContextLine(note, 220)}`).join("\n");
615240
615532
  const tags = card.tags.length ? ` tags:${card.tags.slice(0, 8).join(",")}` : "";
@@ -615247,13 +615539,17 @@ ${notes2}`;
615247
615539
  if (!wantsUserScope) return true;
615248
615540
  if (effectiveUserId !== void 0 && fact.userIds.includes(effectiveUserId)) return true;
615249
615541
  return !!effectiveUsername && fact.usernames.includes(effectiveUsername);
615250
- }).map((fact) => ({
615542
+ }).filter((fact) => inWindow(fact.updatedAt ?? fact.createdAt)).map((fact) => ({
615251
615543
  fact,
615252
615544
  score: telegramMemorySimilarity(
615253
615545
  queryTokens,
615254
615546
  telegramMemoryTokens([fact.text, fact.tags.join(" "), fact.speakers.join(" ")].join(" "))
615255
615547
  ) + (effectiveUserId !== void 0 && fact.userIds.includes(effectiveUserId) ? 0.3 : 0) + (effectiveUsername && fact.usernames.includes(effectiveUsername) ? 0.3 : 0)
615256
- })).filter((item) => item.score > 0).sort((a2, b) => b.score - a2.score || b.fact.updatedAt - a2.fact.updatedAt).slice(0, maxResults) : [];
615548
+ })).filter((item) => item.score > 0 || since !== void 0 || until !== void 0 || bucket !== void 0).sort((a2, b) => {
615549
+ if (bucket === "earliest") return (a2.fact.updatedAt ?? 0) - (b.fact.updatedAt ?? 0);
615550
+ if (bucket === "latest") return (b.fact.updatedAt ?? 0) - (a2.fact.updatedAt ?? 0);
615551
+ return b.score - a2.score || b.fact.updatedAt - a2.fact.updatedAt;
615552
+ }).slice(0, maxResults) : [];
615257
615553
  const rawRows = this.searchTelegramSqliteMirrorRows(msgSessionKey, query, {
615258
615554
  limit: maxResults,
615259
615555
  userId: effectiveUserId,
@@ -615263,8 +615559,9 @@ ${notes2}`;
615263
615559
  const historyForScan = this.chatHistory.get(msgSessionKey) ?? [];
615264
615560
  const lowerQuery = query.toLowerCase().trim();
615265
615561
  const queryWords = lowerQuery.split(/\s+/).filter((w) => w.length >= 3);
615562
+ const hasTimeConstraint = since !== void 0 || until !== void 0 || bucket !== void 0;
615266
615563
  const rawHistoryMatches = [];
615267
- if (historyForScan.length > 0 && lowerQuery.length > 0) {
615564
+ if (historyForScan.length > 0) {
615268
615565
  for (const entry of historyForScan) {
615269
615566
  if (wantsUserScope) {
615270
615567
  const eUser = entry.fromUserId;
@@ -615272,15 +615569,24 @@ ${notes2}`;
615272
615569
  const matchesUser = effectiveUserId !== void 0 && eUser === effectiveUserId || effectiveUsername.length > 0 && eName === effectiveUsername;
615273
615570
  if (!matchesUser) continue;
615274
615571
  }
615572
+ if (!inWindow(entry.ts ?? null)) continue;
615275
615573
  const hay = String(entry.text || "").toLowerCase();
615276
615574
  if (!hay) continue;
615277
- const hasExact = hay.includes(lowerQuery);
615278
- const tokenHits = queryWords.filter((w) => hay.includes(w)).length;
615279
- if (hasExact || queryWords.length > 0 && tokenHits >= Math.min(2, queryWords.length)) {
615575
+ let lexMatch = false;
615576
+ if (lowerQuery.length > 0) {
615577
+ const hasExact = hay.includes(lowerQuery);
615578
+ const tokenHits = queryWords.filter((w) => hay.includes(w)).length;
615579
+ lexMatch = hasExact || queryWords.length > 0 && tokenHits >= Math.min(2, queryWords.length);
615580
+ }
615581
+ if (lexMatch || hasTimeConstraint) {
615280
615582
  rawHistoryMatches.push(entry);
615281
615583
  }
615282
615584
  }
615283
- rawHistoryMatches.sort((a2, b) => (b.ts ?? 0) - (a2.ts ?? 0));
615585
+ if (bucket === "earliest") {
615586
+ rawHistoryMatches.sort((a2, b) => (a2.ts ?? 0) - (b.ts ?? 0));
615587
+ } else {
615588
+ rawHistoryMatches.sort((a2, b) => (b.ts ?? 0) - (a2.ts ?? 0));
615589
+ }
615284
615590
  }
615285
615591
  const rawHistorySliced = rawHistoryMatches.slice(0, maxResults);
615286
615592
  const scopeLabel = wantsUserScope ? `user ${effectiveUsername ? `@${effectiveUsername}` : `id ${effectiveUserId}`}` : `chat ${currentGroupId || msgSessionKey}`;
@@ -615327,8 +615633,9 @@ ${lines.join("\n")}`
615327
615633
  output: `No structured matches for "${query}" in ${scopeLabel}, but the scope IS populated: ${scannedCount} memory cards, ${associativeCount} associative facts, ${historyCount} history entries scanned. Try a broader query (single keyword), a different angle (related concept), or memory_read with topic="${this.telegramScopedTopic(chatId, "general")}" to enumerate. If you're looking for an older turn, ask for the speaker name plus a topic word.`
615328
615634
  };
615329
615635
  }
615636
+ const timeBanner = timeLabel ? ` (time-range: ${timeLabel}${since !== void 0 || until !== void 0 ? `; window=${since !== void 0 ? new Date(since).toISOString() : "−∞"} → ${until !== void 0 ? new Date(until).toISOString() : "+∞"}` : ""})` : "";
615330
615637
  const output = [
615331
- `Scoped Telegram memory search for "${query}" in ${scopeLabel}.`,
615638
+ `Scoped Telegram memory search for "${query}" in ${scopeLabel}${timeBanner}.`,
615332
615639
  "Results are scoped to this Telegram chat and may include raw message evidence, graph episodes, associative facts, and memory cards.",
615333
615640
  "",
615334
615641
  sections.join("\n\n")
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "omnius",
3
- "version": "1.0.65",
3
+ "version": "1.0.66",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "omnius",
9
- "version": "1.0.65",
9
+ "version": "1.0.66",
10
10
  "bundleDependencies": [
11
11
  "image-to-ascii"
12
12
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omnius",
3
- "version": "1.0.65",
3
+ "version": "1.0.66",
4
4
  "description": "AI coding agent powered by open-source models (Ollama/vLLM) — interactive TUI with agentic tool-calling loop",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",