omnius 1.0.63 → 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] ?? "";
@@ -613237,6 +613378,213 @@ ${lines.join("\n")}`);
613237
613378
  return `- ${when} ${speaker}${mode}${messageId}: ${telegramContextJsonString(text, maxText)}`;
613238
613379
  }).join("\n");
613239
613380
  }
613381
+ /**
613382
+ * Build an always-on "memory substrate census" injected into every Telegram
613383
+ * prompt. Anchors the agent against false-empty responses: lists counts for
613384
+ * each store + the chat-scoped memory_read topic filenames that exist on disk.
613385
+ *
613386
+ * Without this, when the lexical scorers (cards/facts/episodes) score zero
613387
+ * for the current query, NO section about those stores reaches the prompt,
613388
+ * and the bot will report "memory empty" in good faith. This section keeps
613389
+ * the substrate visible regardless of per-turn scoring.
613390
+ */
613391
+ buildTelegramMemorySubstrateCensus(sessionKey, msg) {
613392
+ const cardCount = (this.chatMemoryCards.get(sessionKey) ?? []).length;
613393
+ const memory = this.chatAssociativeMemory.get(sessionKey);
613394
+ const factCount = memory?.facts.length ?? 0;
613395
+ const relationshipCount = memory?.relationships.length ?? 0;
613396
+ const userMemoryCount = memory ? Object.keys(memory.users).length : 0;
613397
+ const historyCount = (this.chatHistory.get(sessionKey) ?? []).length;
613398
+ let sqliteCount = 0;
613399
+ let episodeCount2 = 0;
613400
+ try {
613401
+ const db = this.telegramDb();
613402
+ if (db) {
613403
+ const row = db.prepare("SELECT COUNT(*) AS n FROM telegram_messages WHERE session_key = ?").get(sessionKey);
613404
+ sqliteCount = Number(row?.n) || 0;
613405
+ }
613406
+ } catch {
613407
+ }
613408
+ if (this.repoRoot) {
613409
+ try {
613410
+ const paths = omniusMemoryDbPaths(this.repoRoot);
613411
+ if (existsSync113(paths.episodes)) {
613412
+ const graph = new TemporalGraph(paths.knowledge);
613413
+ const store2 = new EpisodeStore(paths.episodes, graph);
613414
+ try {
613415
+ const sample = store2.search({ sessionId: sessionKey, limit: 1 }) ?? [];
613416
+ if (sample.length > 0) {
613417
+ const dbAny = store2.db;
613418
+ if (dbAny && typeof dbAny.prepare === "function") {
613419
+ const row = dbAny.prepare("SELECT COUNT(*) AS n FROM episodes WHERE session_id = ?").get(sessionKey);
613420
+ episodeCount2 = Number(row?.n) || 0;
613421
+ } else {
613422
+ episodeCount2 = 1;
613423
+ }
613424
+ }
613425
+ } finally {
613426
+ try {
613427
+ store2.close();
613428
+ } catch {
613429
+ }
613430
+ try {
613431
+ graph.close();
613432
+ } catch {
613433
+ }
613434
+ }
613435
+ }
613436
+ } catch {
613437
+ }
613438
+ }
613439
+ const chatId = msg.chatId;
613440
+ const topicFiles = [];
613441
+ if (this.repoRoot && chatId !== void 0) {
613442
+ try {
613443
+ const memDir = resolve43(this.repoRoot, ".omnius", "memory");
613444
+ if (existsSync113(memDir)) {
613445
+ const prefix = this.telegramScopedMemoryPrefix(chatId);
613446
+ for (const file of readdirSync40(memDir)) {
613447
+ if (!file.endsWith(".json") || !file.startsWith(prefix)) continue;
613448
+ const topic = file.slice(prefix.length, -".json".length);
613449
+ if (topic) topicFiles.push(topic);
613450
+ }
613451
+ }
613452
+ } catch {
613453
+ }
613454
+ }
613455
+ const anyMemory = cardCount + factCount + relationshipCount + userMemoryCount + sqliteCount + episodeCount2 + topicFiles.length > 0;
613456
+ if (!anyMemory && historyCount === 0) return "";
613457
+ const lines = [
613458
+ "### Scoped Memory Substrate (this chat — ALWAYS PRESENT)",
613459
+ "Persistent memory IS available for this chat. The current turn's lexical scorers may",
613460
+ "have surfaced 0 matches above — that does NOT mean the substrate is empty. Counts:",
613461
+ `- Memory cards: ${cardCount}`,
613462
+ `- Associative facts: ${factCount}`,
613463
+ `- Associative relationships: ${relationshipCount}`,
613464
+ `- Per-user memories: ${userMemoryCount}`,
613465
+ `- Rolling history entries retained: ${historyCount}`,
613466
+ `- SQLite mirror rows: ${sqliteCount}`,
613467
+ `- Episodes (durable, day+ scope): ${episodeCount2}`
613468
+ ];
613469
+ if (topicFiles.length > 0) {
613470
+ lines.push("");
613471
+ lines.push("Per-topic memory files (call memory_read with one of these `topic` values):");
613472
+ for (const topic of topicFiles.slice(0, 80)) {
613473
+ lines.push(` - ${topic}`);
613474
+ }
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
+ }
613579
+ lines.push("");
613580
+ lines.push("RULES:");
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.");
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.");
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.");
613586
+ return lines.join("\n");
613587
+ }
613240
613588
  buildTelegramConversationContextStream(sessionKey, msg, maxRecent = TELEGRAM_CONTEXT_RECENT_DEFAULT) {
613241
613589
  this.ensureTelegramConversationLoaded(sessionKey);
613242
613590
  const history = this.chatHistory.get(sessionKey) ?? [];
@@ -613260,6 +613608,11 @@ ${lines.join("\n")}`);
613260
613608
  if (currentReplyContext) {
613261
613609
  sections.push(currentReplyContext);
613262
613610
  }
613611
+ try {
613612
+ const substrateCensus = this.buildTelegramMemorySubstrateCensus(sessionKey, msg);
613613
+ if (substrateCensus) sections.push(substrateCensus);
613614
+ } catch {
613615
+ }
613263
613616
  const scopedPersonality = this.renderTelegramScopedPersonality(sessionKey, msg);
613264
613617
  if (scopedPersonality) {
613265
613618
  sections.push(scopedPersonality);
@@ -615073,7 +615426,7 @@ ${result.llmContent ?? result.output}` };
615073
615426
  if (tool.name === "memory_search") {
615074
615427
  return {
615075
615428
  ...tool,
615076
- 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.",
615077
615430
  parameters: (() => {
615078
615431
  const base3 = tool.parameters ?? {};
615079
615432
  const props = base3["properties"] && typeof base3["properties"] === "object" && !Array.isArray(base3["properties"]) ? base3["properties"] : {};
@@ -615084,15 +615437,57 @@ ${result.llmContent ?? result.output}` };
615084
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." },
615085
615438
  user_id: { type: "number", description: "Optional Telegram user id to search within the current group only." },
615086
615439
  username: { type: "string", description: "Optional Telegram username to search within the current group only." },
615087
- 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." }
615088
615444
  }
615089
615445
  };
615090
615446
  })(),
615091
615447
  execute: async (args) => {
615092
- const query = String(args["query"] || "").trim();
615448
+ const rawQuery = String(args["query"] || "").trim();
615093
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;
615094
- if (!query) return { success: true, output: "Search query is required." };
615450
+ if (!rawQuery) return { success: true, output: "Search query is required." };
615095
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();
615096
615491
  const currentGroupId = chatId === void 0 ? "" : String(chatId);
615097
615492
  const requestedGroupId = String(args["group_id"] ?? args["chat_id"] ?? "").trim();
615098
615493
  if (requestedGroupId && currentGroupId && requestedGroupId !== currentGroupId) {
@@ -615121,13 +615516,17 @@ ${result.llmContent ?? result.output}` };
615121
615516
  return false;
615122
615517
  });
615123
615518
  const queryTokens = telegramMemoryTokens(query);
615124
- const cardResults = cards.map((card) => ({
615519
+ const cardResults = cards.filter((card) => inWindow(card.updatedAt ?? card.createdAt)).map((card) => ({
615125
615520
  card,
615126
615521
  score: telegramMemorySimilarity(
615127
615522
  queryTokens,
615128
615523
  telegramMemoryTokens([card.title, card.tags.join(" "), card.speakers.join(" "), card.notes.join(" ")].join(" "))
615129
615524
  )
615130
- })).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);
615131
615530
  const cardLines = cardResults.map(({ card, score }) => {
615132
615531
  const notes2 = card.notes.slice(-4).map((note) => ` - ${truncateTelegramContextLine(note, 220)}`).join("\n");
615133
615532
  const tags = card.tags.length ? ` tags:${card.tags.slice(0, 8).join(",")}` : "";
@@ -615140,13 +615539,17 @@ ${notes2}`;
615140
615539
  if (!wantsUserScope) return true;
615141
615540
  if (effectiveUserId !== void 0 && fact.userIds.includes(effectiveUserId)) return true;
615142
615541
  return !!effectiveUsername && fact.usernames.includes(effectiveUsername);
615143
- }).map((fact) => ({
615542
+ }).filter((fact) => inWindow(fact.updatedAt ?? fact.createdAt)).map((fact) => ({
615144
615543
  fact,
615145
615544
  score: telegramMemorySimilarity(
615146
615545
  queryTokens,
615147
615546
  telegramMemoryTokens([fact.text, fact.tags.join(" "), fact.speakers.join(" ")].join(" "))
615148
615547
  ) + (effectiveUserId !== void 0 && fact.userIds.includes(effectiveUserId) ? 0.3 : 0) + (effectiveUsername && fact.usernames.includes(effectiveUsername) ? 0.3 : 0)
615149
- })).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) : [];
615150
615553
  const rawRows = this.searchTelegramSqliteMirrorRows(msgSessionKey, query, {
615151
615554
  limit: maxResults,
615152
615555
  userId: effectiveUserId,
@@ -615156,8 +615559,9 @@ ${notes2}`;
615156
615559
  const historyForScan = this.chatHistory.get(msgSessionKey) ?? [];
615157
615560
  const lowerQuery = query.toLowerCase().trim();
615158
615561
  const queryWords = lowerQuery.split(/\s+/).filter((w) => w.length >= 3);
615562
+ const hasTimeConstraint = since !== void 0 || until !== void 0 || bucket !== void 0;
615159
615563
  const rawHistoryMatches = [];
615160
- if (historyForScan.length > 0 && lowerQuery.length > 0) {
615564
+ if (historyForScan.length > 0) {
615161
615565
  for (const entry of historyForScan) {
615162
615566
  if (wantsUserScope) {
615163
615567
  const eUser = entry.fromUserId;
@@ -615165,15 +615569,24 @@ ${notes2}`;
615165
615569
  const matchesUser = effectiveUserId !== void 0 && eUser === effectiveUserId || effectiveUsername.length > 0 && eName === effectiveUsername;
615166
615570
  if (!matchesUser) continue;
615167
615571
  }
615572
+ if (!inWindow(entry.ts ?? null)) continue;
615168
615573
  const hay = String(entry.text || "").toLowerCase();
615169
615574
  if (!hay) continue;
615170
- const hasExact = hay.includes(lowerQuery);
615171
- const tokenHits = queryWords.filter((w) => hay.includes(w)).length;
615172
- 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) {
615173
615582
  rawHistoryMatches.push(entry);
615174
615583
  }
615175
615584
  }
615176
- 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
+ }
615177
615590
  }
615178
615591
  const rawHistorySliced = rawHistoryMatches.slice(0, maxResults);
615179
615592
  const scopeLabel = wantsUserScope ? `user ${effectiveUsername ? `@${effectiveUsername}` : `id ${effectiveUserId}`}` : `chat ${currentGroupId || msgSessionKey}`;
@@ -615220,8 +615633,9 @@ ${lines.join("\n")}`
615220
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.`
615221
615634
  };
615222
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() : "+∞"}` : ""})` : "";
615223
615637
  const output = [
615224
- `Scoped Telegram memory search for "${query}" in ${scopeLabel}.`,
615638
+ `Scoped Telegram memory search for "${query}" in ${scopeLabel}${timeBanner}.`,
615225
615639
  "Results are scoped to this Telegram chat and may include raw message evidence, graph episodes, associative facts, and memory cards.",
615226
615640
  "",
615227
615641
  sections.join("\n\n")
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "omnius",
3
- "version": "1.0.63",
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.63",
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.63",
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",