@loreai/core 0.10.2 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/bun/config.d.ts +8 -0
  2. package/dist/bun/config.d.ts.map +1 -1
  3. package/dist/bun/db.d.ts.map +1 -1
  4. package/dist/bun/distillation.d.ts +74 -2
  5. package/dist/bun/distillation.d.ts.map +1 -1
  6. package/dist/bun/embedding.d.ts.map +1 -1
  7. package/dist/bun/gradient.d.ts +72 -0
  8. package/dist/bun/gradient.d.ts.map +1 -1
  9. package/dist/bun/index.d.ts +4 -2
  10. package/dist/bun/index.d.ts.map +1 -1
  11. package/dist/bun/index.js +554 -76
  12. package/dist/bun/index.js.map +4 -4
  13. package/dist/bun/prompt.d.ts +8 -2
  14. package/dist/bun/prompt.d.ts.map +1 -1
  15. package/dist/bun/temporal.d.ts +31 -0
  16. package/dist/bun/temporal.d.ts.map +1 -1
  17. package/dist/bun/types.d.ts +9 -0
  18. package/dist/bun/types.d.ts.map +1 -1
  19. package/dist/bun/worker-model.d.ts +90 -0
  20. package/dist/bun/worker-model.d.ts.map +1 -0
  21. package/dist/node/config.d.ts +8 -0
  22. package/dist/node/config.d.ts.map +1 -1
  23. package/dist/node/db.d.ts.map +1 -1
  24. package/dist/node/distillation.d.ts +74 -2
  25. package/dist/node/distillation.d.ts.map +1 -1
  26. package/dist/node/embedding.d.ts.map +1 -1
  27. package/dist/node/gradient.d.ts +72 -0
  28. package/dist/node/gradient.d.ts.map +1 -1
  29. package/dist/node/index.d.ts +4 -2
  30. package/dist/node/index.d.ts.map +1 -1
  31. package/dist/node/index.js +554 -76
  32. package/dist/node/index.js.map +4 -4
  33. package/dist/node/prompt.d.ts +8 -2
  34. package/dist/node/prompt.d.ts.map +1 -1
  35. package/dist/node/temporal.d.ts +31 -0
  36. package/dist/node/temporal.d.ts.map +1 -1
  37. package/dist/node/types.d.ts +9 -0
  38. package/dist/node/types.d.ts.map +1 -1
  39. package/dist/node/worker-model.d.ts +90 -0
  40. package/dist/node/worker-model.d.ts.map +1 -0
  41. package/dist/types/config.d.ts +8 -0
  42. package/dist/types/config.d.ts.map +1 -1
  43. package/dist/types/db.d.ts.map +1 -1
  44. package/dist/types/distillation.d.ts +74 -2
  45. package/dist/types/distillation.d.ts.map +1 -1
  46. package/dist/types/embedding.d.ts.map +1 -1
  47. package/dist/types/gradient.d.ts +72 -0
  48. package/dist/types/gradient.d.ts.map +1 -1
  49. package/dist/types/index.d.ts +4 -2
  50. package/dist/types/index.d.ts.map +1 -1
  51. package/dist/types/prompt.d.ts +8 -2
  52. package/dist/types/prompt.d.ts.map +1 -1
  53. package/dist/types/temporal.d.ts +31 -0
  54. package/dist/types/temporal.d.ts.map +1 -1
  55. package/dist/types/types.d.ts +9 -0
  56. package/dist/types/types.d.ts.map +1 -1
  57. package/dist/types/worker-model.d.ts +90 -0
  58. package/dist/types/worker-model.d.ts.map +1 -0
  59. package/package.json +1 -1
  60. package/src/config.ts +53 -6
  61. package/src/db.ts +68 -6
  62. package/src/distillation.ts +225 -28
  63. package/src/embedding.ts +7 -0
  64. package/src/gradient.ts +305 -17
  65. package/src/index.ts +16 -0
  66. package/src/lat-reader.ts +4 -4
  67. package/src/ltm.ts +17 -17
  68. package/src/prompt.ts +101 -0
  69. package/src/recall.ts +4 -4
  70. package/src/temporal.ts +41 -10
  71. package/src/types.ts +9 -0
  72. package/src/worker-model.ts +264 -0
@@ -122,9 +122,11 @@ var require_extend = __commonJS({
122
122
  // src/temporal.ts
123
123
  var temporal_exports = {};
124
124
  __export(temporal_exports, {
125
+ CHUNK_TERMINATOR: () => CHUNK_TERMINATOR,
125
126
  bySession: () => bySession,
126
127
  count: () => count,
127
128
  markDistilled: () => markDistilled,
129
+ partsToText: () => partsToText,
128
130
  prune: () => prune,
129
131
  search: () => search2,
130
132
  searchScored: () => searchScored,
@@ -436,6 +438,58 @@ var MIGRATIONS = [
436
438
  to_id TEXT NOT NULL REFERENCES knowledge(id) ON DELETE CASCADE,
437
439
  PRIMARY KEY (from_id, to_id)
438
440
  );
441
+ `,
442
+ `
443
+ -- Version 11: F3b -- unambiguous chunk terminator in temporal_messages.content.
444
+ --
445
+ -- Pre-F3b, partsToText joined chunks with a newline. Tool-output payloads
446
+ -- can contain newlines too, so the boundary between a tool envelope and a
447
+ -- following plain-text or [reasoning] chunk was structurally ambiguous.
448
+ -- This caused two known limitations in the F3 distill-input truncator:
449
+ -- trailing text could be swallowed into a tool payload, and embedded
450
+ -- literal envelope strings inside a payload (e.g. when reading AGENTS.md)
451
+ -- could fabricate fake boundaries.
452
+ --
453
+ -- F3b switches the chunk separator to newline plus ASCII Unit Separator
454
+ -- (char 31). The Unit Separator is non-word so FTS5's unicode61 tokenizer
455
+ -- ignores it (zero BM25 impact). New rows are written via the post-F3b
456
+ -- partsToText. Existing rows are rewritten in place by the UPDATE below,
457
+ -- which uses pure SQL replace() to inject the Unit Separator after every
458
+ -- legacy chunk-prefix sequence -- the same boundary patterns the legacy
459
+ -- F3 reader was already trying to recover.
460
+ --
461
+ -- Trade-off (acceptable): any embedded legacy chunk-prefix sequence
462
+ -- inside a tool payload becomes a structural boundary post-migration.
463
+ -- This matches what the legacy F3 reader did at read-time anyway, baked
464
+ -- into the row permanently. The migration runs once per machine.
465
+ --
466
+ -- Idempotent: a row that already contains the Unit Separator before a
467
+ -- chunk prefix no longer matches the search literal (the separator
468
+ -- interposes), so re-running the UPDATE is a no-op for migrated rows.
469
+ -- (Important: migrate() in db.ts runs each migration via database.exec()
470
+ -- with no explicit BEGIN/COMMIT around the whole loop. SQLite makes this
471
+ -- single UPDATE statement atomic per-statement, so partial progress on
472
+ -- crash is safe to retry thanks to the idempotency above.)
473
+ --
474
+ -- char(10) = newline, char(31) = Unit Separator. SQLite has no native
475
+ -- regex, but two nested replace() calls on the literal prefixes are
476
+ -- sufficient because both legacy chunk prefixes match at line-start.
477
+ --
478
+ -- Each row UPDATE fires the temporal_fts_update trigger once; because
479
+ -- the Unit Separator is a non-word character, the re-indexed content
480
+ -- tokenizes identically -- net no-op for FTS scoring.
481
+ UPDATE temporal_messages
482
+ SET content = replace(
483
+ replace(
484
+ content,
485
+ char(10) || '[tool:',
486
+ char(10) || char(31) || '[tool:'
487
+ ),
488
+ char(10) || '[reasoning] ',
489
+ char(10) || char(31) || '[reasoning] '
490
+ )
491
+ WHERE content LIKE '%' || char(10) || '[tool:%'
492
+ OR content LIKE '%' || char(10) || '[reasoning] %';
439
493
  `
440
494
  ];
441
495
  function dataDir() {
@@ -456,11 +510,13 @@ function db() {
456
510
  mkdirSync(dir, { recursive: true });
457
511
  path = join(dir, "lore.db");
458
512
  }
459
- instance = new Database(path);
460
- instance.exec("PRAGMA journal_mode = WAL");
461
- instance.exec("PRAGMA foreign_keys = ON");
462
- instance.exec("PRAGMA auto_vacuum = INCREMENTAL");
463
- migrate(instance);
513
+ const database = new Database(path);
514
+ database.exec("PRAGMA journal_mode = WAL");
515
+ database.exec("PRAGMA foreign_keys = ON");
516
+ database.exec("PRAGMA busy_timeout = 5000");
517
+ database.exec("PRAGMA auto_vacuum = INCREMENTAL");
518
+ migrate(database);
519
+ instance = database;
464
520
  return instance;
465
521
  }
466
522
  var VACUUM_MIGRATION_INDEX = 2;
@@ -10765,12 +10821,27 @@ EXACT NUMBERS: When two segments report different numbers for what seems like th
10765
10821
 
10766
10822
  EARLY-SESSION CONTENT: Bug fixes, code changes, and decisions from the start of a session are just as important as later work. Never drop them just because the segment is short or old. If the first segment contains a specific bug fix with file paths and root cause, it MUST survive into the reflection.
10767
10823
 
10824
+ ANCHORED UPDATES: If the prompt includes a <previous-meta-summary> block, treat it as the current consolidated state. Update it using the NEW observation segments \u2014 preserve still-true details, remove stale details, and merge in new facts. Keep the same section headings. Do NOT re-derive unchanged sections verbatim unless the new segments contradict them.
10825
+
10768
10826
  Output ONLY an <observations> block with the consolidated observations.`;
10769
- function recursiveUser(distillations) {
10827
+ function recursiveUser(distillations, previousMeta) {
10770
10828
  const entries = distillations.map(
10771
10829
  (d, i) => `Segment ${i + 1}:
10772
10830
  ${d.observations}`
10773
10831
  );
10832
+ if (previousMeta) {
10833
+ return `Update the anchored meta-summary below using the NEW observation segments. Preserve still-true details, remove stale details, and merge in new facts. Keep the same section headings.
10834
+
10835
+ <previous-meta-summary>
10836
+ ${previousMeta}
10837
+ </previous-meta-summary>
10838
+
10839
+ ---
10840
+
10841
+ New observation segments to merge (chronological order):
10842
+
10843
+ ${entries.join("\n\n---\n\n")}`;
10844
+ }
10774
10845
  return `Observation segments to consolidate (chronological order):
10775
10846
 
10776
10847
  ${entries.join("\n\n---\n\n")}`;
@@ -10925,6 +10996,61 @@ function formatDistillations(distillations) {
10925
10996
  }
10926
10997
  return sections.join("\n\n");
10927
10998
  }
10999
+ var COMPACT_SUMMARY_TEMPLATE = `Output exactly this Markdown structure. Keep every section in this order, even when empty (use "(none)").
11000
+
11001
+ ---
11002
+ ## Goal
11003
+ - [single-sentence task summary]
11004
+
11005
+ ## Constraints & Preferences
11006
+ - [user constraints, preferences, specs, or "(none)"]
11007
+
11008
+ ## Progress
11009
+ ### Done
11010
+ - [completed work or "(none)"]
11011
+
11012
+ ### In Progress
11013
+ - [current work or "(none)"]
11014
+
11015
+ ### Blocked
11016
+ - [blockers or "(none)"]
11017
+
11018
+ ## Key Decisions
11019
+ - [decision and why, or "(none)"]
11020
+
11021
+ ## Next Steps
11022
+ - [ordered next actions or "(none)"]
11023
+
11024
+ ## Critical Context
11025
+ - [important technical facts, errors, open questions, or "(none)"]
11026
+
11027
+ ## Relevant Files
11028
+ - [file or directory path: why it matters, or "(none)"]
11029
+ ---
11030
+
11031
+ Rules:
11032
+ - Keep every section, even when empty.
11033
+ - Use terse bullets, not prose paragraphs.
11034
+ - Preserve exact file paths, commands, error strings, and identifiers when known.
11035
+ - Do not mention the summary process or that context was compacted.
11036
+ - End with "I'm ready to continue." on its own line after the closing "---".`;
11037
+ function buildCompactPrompt(input) {
11038
+ const distillSection = input.hasDistillations ? "Lore has pre-computed chunked summaries of the session history (injected above as context). Use them as the authoritative source \u2014 do NOT re-read raw conversation messages that conflict with them.\n\n" : "";
11039
+ const anchorBlock = input.previousSummary ? `A prior compacted summary exists for this session. Update it using the conversation history above: preserve still-true details, remove stale details, and merge in new facts. Keep every section in place.
11040
+
11041
+ <previous-summary>
11042
+ ${input.previousSummary}
11043
+ </previous-summary>
11044
+
11045
+ ` : "";
11046
+ const knowledgeBlock = input.knowledge ? `
11047
+ ${input.knowledge}
11048
+ ` : "";
11049
+ return `You are producing a compacted session summary for an AI coding agent. This summary will be the ONLY context available in the next part of the conversation.
11050
+
11051
+ ${distillSection}${anchorBlock}${COMPACT_SUMMARY_TEMPLATE}
11052
+ ${knowledgeBlock}`;
11053
+ }
10928
11054
  function estimateTokens(text4) {
10929
11055
  return Math.ceil(text4.length / 3);
10930
11056
  }
@@ -11177,6 +11303,7 @@ function isToolPart(p3) {
11177
11303
  function estimate(text4) {
11178
11304
  return Math.ceil(text4.length / 3);
11179
11305
  }
11306
+ var CHUNK_TERMINATOR = "";
11180
11307
  function partsToText(parts) {
11181
11308
  const chunks = [];
11182
11309
  for (const part of parts) {
@@ -11186,7 +11313,7 @@ function partsToText(parts) {
11186
11313
  else if (isToolPart(part) && part.state.status === "completed")
11187
11314
  chunks.push(`[tool:${part.tool}] ${part.state.output}`);
11188
11315
  }
11189
- return sanitizeSurrogates(chunks.join("\n"));
11316
+ return sanitizeSurrogates(chunks.join("\n" + CHUNK_TERMINATOR));
11190
11317
  }
11191
11318
  function messageMetadata(info2, parts) {
11192
11319
  const meta3 = {};
@@ -11265,11 +11392,11 @@ function search2(input) {
11265
11392
  const limit = input.limit ?? 20;
11266
11393
  const q = ftsQuery(input.query);
11267
11394
  if (q === EMPTY_QUERY) return [];
11268
- const ftsSQL = input.sessionID ? `SELECT m.* FROM temporal_messages m
11269
- JOIN temporal_fts f ON m.rowid = f.rowid
11395
+ const ftsSQL = input.sessionID ? `SELECT m.* FROM temporal_fts f
11396
+ CROSS JOIN temporal_messages m ON m.rowid = f.rowid
11270
11397
  WHERE f.content MATCH ? AND m.project_id = ? AND m.session_id = ?
11271
- ORDER BY rank LIMIT ?` : `SELECT m.* FROM temporal_messages m
11272
- JOIN temporal_fts f ON m.rowid = f.rowid
11398
+ ORDER BY rank LIMIT ?` : `SELECT m.* FROM temporal_fts f
11399
+ CROSS JOIN temporal_messages m ON m.rowid = f.rowid
11273
11400
  WHERE f.content MATCH ? AND m.project_id = ?
11274
11401
  ORDER BY rank LIMIT ?`;
11275
11402
  const params = input.sessionID ? [q, pid, input.sessionID, limit] : [q, pid, limit];
@@ -11294,11 +11421,11 @@ function searchScored(input) {
11294
11421
  const limit = input.limit ?? 20;
11295
11422
  const q = ftsQuery(input.query);
11296
11423
  if (q === EMPTY_QUERY) return [];
11297
- const ftsSQL = input.sessionID ? `SELECT m.*, rank FROM temporal_messages m
11298
- JOIN temporal_fts f ON m.rowid = f.rowid
11424
+ const ftsSQL = input.sessionID ? `SELECT m.*, rank FROM temporal_fts f
11425
+ CROSS JOIN temporal_messages m ON m.rowid = f.rowid
11299
11426
  WHERE f.content MATCH ? AND m.project_id = ? AND m.session_id = ?
11300
- ORDER BY rank LIMIT ?` : `SELECT m.*, rank FROM temporal_messages m
11301
- JOIN temporal_fts f ON m.rowid = f.rowid
11427
+ ORDER BY rank LIMIT ?` : `SELECT m.*, rank FROM temporal_fts f
11428
+ CROSS JOIN temporal_messages m ON m.rowid = f.rowid
11302
11429
  WHERE f.content MATCH ? AND m.project_id = ?
11303
11430
  ORDER BY rank LIMIT ?`;
11304
11431
  const params = input.sessionID ? [q, pid, input.sessionID, limit] : [q, pid, limit];
@@ -25500,18 +25627,63 @@ var LoreConfig = external_exports.object({
25500
25627
  providerID: external_exports.string(),
25501
25628
  modelID: external_exports.string()
25502
25629
  }).optional(),
25630
+ /** Explicit worker model override. When set, all background workers (distillation,
25631
+ * curation, query expansion) use this model instead of the session model or the
25632
+ * auto-selected worker model. Bypasses dynamic worker model selection entirely. */
25633
+ workerModel: external_exports.object({
25634
+ providerID: external_exports.string(),
25635
+ modelID: external_exports.string()
25636
+ }).optional(),
25503
25637
  budget: external_exports.object({
25504
25638
  distilled: external_exports.number().min(0.05).max(0.5).default(0.25),
25505
25639
  raw: external_exports.number().min(0.1).max(0.7).default(0.4),
25506
25640
  output: external_exports.number().min(0.1).max(0.5).default(0.25),
25507
- /** Max fraction of usable context reserved for LTM system-prompt injection. Default: 0.10 (10%). */
25508
- ltm: external_exports.number().min(0.02).max(0.3).default(0.1)
25509
- }).default({ distilled: 0.25, raw: 0.4, output: 0.25, ltm: 0.1 }),
25641
+ /** Max fraction of usable context reserved for LTM system-prompt injection. Default: 0.05 (5%). */
25642
+ ltm: external_exports.number().min(0.02).max(0.3).default(0.05),
25643
+ /** Per-turn cache-read cost target in dollars. Controls when layer 0 (full
25644
+ * passthrough) escalates to layer 1 (compressed). The cap is derived as:
25645
+ * maxLayer0Tokens = max(target / model.cost.cache.read, 40K).
25646
+ * Lower = cheaper but earlier compression. Default: 0.10. Set to 0 to
25647
+ * disable cost-aware capping (use the model's full context). */
25648
+ targetCacheReadCostPerTurn: external_exports.number().min(0).default(0.1),
25649
+ /** Direct override for the layer-0 token cap. When set, bypasses the
25650
+ * cost-aware formula from targetCacheReadCostPerTurn. 0 = disabled
25651
+ * (no cap, use full context). Default: undefined (use cost-aware auto). */
25652
+ maxLayer0Tokens: external_exports.number().min(0).optional()
25653
+ }).default({ distilled: 0.25, raw: 0.4, output: 0.25, ltm: 0.05, targetCacheReadCostPerTurn: 0.1 }),
25654
+ /**
25655
+ * Cold-cache idle-resume handling.
25656
+ *
25657
+ * Anthropic's prompt cache evicts entries after ~5 min (default tier) /
25658
+ * ~1 hour (extended tier). When a session resumes after the eviction window,
25659
+ * Lore's byte-identity caches (distilled prefix, raw window pin, LTM block)
25660
+ * are providing no value because the underlying provider cache is already
25661
+ * cold. On detection, Lore refreshes those caches so the next turn can
25662
+ * produce a better-fitting window without paying a cache cost it would
25663
+ * otherwise be trying to preserve. Reasoning blocks are NOT touched —
25664
+ * Anthropic's April 23 postmortem identified dropping reasoning blocks as
25665
+ * the root cause of forgetfulness/repetition.
25666
+ *
25667
+ * `idleResumeMinutes` is the threshold in minutes. Default 60 — matches
25668
+ * Anthropic's extended-cache eviction window, conservative across providers.
25669
+ * Set to 0 to disable the feature.
25670
+ */
25671
+ idleResumeMinutes: external_exports.number().min(0).max(24 * 60).default(60),
25510
25672
  distillation: external_exports.object({
25511
- minMessages: external_exports.number().min(3).default(8),
25512
- maxSegment: external_exports.number().min(5).default(50),
25513
- metaThreshold: external_exports.number().min(3).default(10)
25514
- }).default({ minMessages: 8, maxSegment: 50, metaThreshold: 10 }),
25673
+ minMessages: external_exports.number().min(3).default(5),
25674
+ maxSegment: external_exports.number().min(5).default(30),
25675
+ metaThreshold: external_exports.number().min(3).default(10),
25676
+ /** Max chars per tool output when rendering temporal messages for distillation input.
25677
+ * Outputs longer than this are replaced with a compact annotation preserving line
25678
+ * count, error signals, and file paths. Default: 2000 (matches upstream OpenCode's
25679
+ * TOOL_OUTPUT_MAX_CHARS during compaction). Set to 0 to disable. */
25680
+ toolOutputMaxChars: external_exports.number().min(0).default(2e3)
25681
+ }).default({
25682
+ minMessages: 5,
25683
+ maxSegment: 30,
25684
+ metaThreshold: 10,
25685
+ toolOutputMaxChars: 2e3
25686
+ }),
25515
25687
  knowledge: external_exports.object({
25516
25688
  /** Set to false to disable long-term knowledge storage and system-prompt injection.
25517
25689
  * Conversation recall (temporal search, distillation search) and context management
@@ -25616,6 +25788,7 @@ __export(embedding_exports, {
25616
25788
  vectorSearch: () => vectorSearch,
25617
25789
  vectorSearchDistillations: () => vectorSearchDistillations
25618
25790
  });
25791
+ var EMBED_TIMEOUT_MS = 1e4;
25619
25792
  var VOYAGE_API_URL = "https://api.voyageai.com/v1/embeddings";
25620
25793
  var VoyageProvider = class {
25621
25794
  maxBatchSize = 128;
@@ -25639,7 +25812,8 @@ var VoyageProvider = class {
25639
25812
  model: this.model,
25640
25813
  input_type: inputType,
25641
25814
  output_dimension: this.dimensions
25642
- })
25815
+ }),
25816
+ signal: AbortSignal.timeout(EMBED_TIMEOUT_MS)
25643
25817
  });
25644
25818
  if (!res.ok) {
25645
25819
  const body = await res.text().catch(() => "");
@@ -25675,7 +25849,8 @@ var OpenAIProvider = class {
25675
25849
  "Content-Type": "application/json",
25676
25850
  Authorization: `Bearer ${this.apiKey}`
25677
25851
  },
25678
- body: JSON.stringify(body)
25852
+ body: JSON.stringify(body),
25853
+ signal: AbortSignal.timeout(EMBED_TIMEOUT_MS)
25679
25854
  });
25680
25855
  if (!res.ok) {
25681
25856
  const responseBody = await res.text().catch(() => "");
@@ -26058,8 +26233,8 @@ function searchScored2(input) {
26058
26233
  const ftsSQL = `SELECT s.id, s.project_id, s.file, s.heading, s.depth, s.content,
26059
26234
  s.content_hash, s.first_paragraph, s.updated_at,
26060
26235
  bm25(lat_sections_fts, 6.0, 2.0) as rank
26061
- FROM lat_sections s
26062
- JOIN lat_sections_fts f ON s.rowid = f.rowid
26236
+ FROM lat_sections_fts f
26237
+ CROSS JOIN lat_sections s ON s.rowid = f.rowid
26063
26238
  WHERE lat_sections_fts MATCH ?
26064
26239
  AND s.project_id = ?
26065
26240
  ORDER BY rank LIMIT ?`;
@@ -26085,8 +26260,8 @@ function scoreForSession(projectPath, sessionContext, maxTokens) {
26085
26260
  `SELECT s.id, s.project_id, s.file, s.heading, s.depth, s.content,
26086
26261
  s.content_hash, s.first_paragraph, s.updated_at,
26087
26262
  bm25(lat_sections_fts, 6.0, 2.0) as rank
26088
- FROM lat_sections s
26089
- JOIN lat_sections_fts f ON s.rowid = f.rowid
26263
+ FROM lat_sections_fts f
26264
+ CROSS JOIN lat_sections s ON s.rowid = f.rowid
26090
26265
  WHERE lat_sections_fts MATCH ?
26091
26266
  AND s.project_id = ?
26092
26267
  ORDER BY rank`
@@ -26215,10 +26390,10 @@ function scoreEntriesFTS(sessionContext) {
26215
26390
  try {
26216
26391
  const results = db().query(
26217
26392
  `SELECT k.id, bm25(knowledge_fts, ?, ?, ?) as rank
26218
- FROM knowledge k
26219
- JOIN knowledge_fts f ON k.rowid = f.rowid
26220
- WHERE knowledge_fts MATCH ?
26221
- AND k.confidence > 0.2`
26393
+ FROM knowledge_fts f
26394
+ CROSS JOIN knowledge k ON k.rowid = f.rowid
26395
+ WHERE knowledge_fts MATCH ?
26396
+ AND k.confidence > 0.2`
26222
26397
  ).all(title, content3, category, q);
26223
26398
  if (!results.length) return /* @__PURE__ */ new Map();
26224
26399
  const ranks = results.map((r) => r.rank);
@@ -26352,13 +26527,13 @@ function search3(input) {
26352
26527
  const q = ftsQuery(input.query);
26353
26528
  if (q === EMPTY_QUERY) return [];
26354
26529
  const pid = input.projectPath ? ensureProject(input.projectPath) : null;
26355
- const ftsSQL = pid ? `SELECT ${KNOWLEDGE_COLS_K} FROM knowledge k
26356
- JOIN knowledge_fts f ON k.rowid = f.rowid
26530
+ const ftsSQL = pid ? `SELECT ${KNOWLEDGE_COLS_K} FROM knowledge_fts f
26531
+ CROSS JOIN knowledge k ON k.rowid = f.rowid
26357
26532
  WHERE knowledge_fts MATCH ?
26358
26533
  AND (k.project_id = ? OR k.project_id IS NULL OR k.cross_project = 1)
26359
26534
  AND k.confidence > 0.2
26360
- ORDER BY bm25(knowledge_fts, ?, ?, ?) LIMIT ?` : `SELECT ${KNOWLEDGE_COLS_K} FROM knowledge k
26361
- JOIN knowledge_fts f ON k.rowid = f.rowid
26535
+ ORDER BY bm25(knowledge_fts, ?, ?, ?) LIMIT ?` : `SELECT ${KNOWLEDGE_COLS_K} FROM knowledge_fts f
26536
+ CROSS JOIN knowledge k ON k.rowid = f.rowid
26362
26537
  WHERE knowledge_fts MATCH ?
26363
26538
  AND k.confidence > 0.2
26364
26539
  ORDER BY bm25(knowledge_fts, ?, ?, ?) LIMIT ?`;
@@ -26385,13 +26560,13 @@ function searchScored3(input) {
26385
26560
  if (q === EMPTY_QUERY) return [];
26386
26561
  const pid = input.projectPath ? ensureProject(input.projectPath) : null;
26387
26562
  const { title, content: content3, category } = ftsWeights();
26388
- const ftsSQL = pid ? `SELECT ${KNOWLEDGE_COLS_K}, bm25(knowledge_fts, ?, ?, ?) as rank FROM knowledge k
26389
- JOIN knowledge_fts f ON k.rowid = f.rowid
26563
+ const ftsSQL = pid ? `SELECT ${KNOWLEDGE_COLS_K}, bm25(knowledge_fts, ?, ?, ?) as rank FROM knowledge_fts f
26564
+ CROSS JOIN knowledge k ON k.rowid = f.rowid
26390
26565
  WHERE knowledge_fts MATCH ?
26391
26566
  AND (k.project_id = ? OR k.project_id IS NULL OR k.cross_project = 1)
26392
26567
  AND k.confidence > 0.2
26393
- ORDER BY rank LIMIT ?` : `SELECT ${KNOWLEDGE_COLS_K}, bm25(knowledge_fts, ?, ?, ?) as rank FROM knowledge k
26394
- JOIN knowledge_fts f ON k.rowid = f.rowid
26568
+ ORDER BY rank LIMIT ?` : `SELECT ${KNOWLEDGE_COLS_K}, bm25(knowledge_fts, ?, ?, ?) as rank FROM knowledge_fts f
26569
+ CROSS JOIN knowledge k ON k.rowid = f.rowid
26395
26570
  WHERE knowledge_fts MATCH ?
26396
26571
  AND k.confidence > 0.2
26397
26572
  ORDER BY rank LIMIT ?`;
@@ -26413,8 +26588,8 @@ function searchScoredOtherProjects(input) {
26413
26588
  if (q === EMPTY_QUERY) return [];
26414
26589
  const excludePid = ensureProject(input.excludeProjectPath);
26415
26590
  const { title, content: content3, category } = ftsWeights();
26416
- const ftsSQL = `SELECT ${KNOWLEDGE_COLS_K}, bm25(knowledge_fts, ?, ?, ?) as rank FROM knowledge k
26417
- JOIN knowledge_fts f ON k.rowid = f.rowid
26591
+ const ftsSQL = `SELECT ${KNOWLEDGE_COLS_K}, bm25(knowledge_fts, ?, ?, ?) as rank FROM knowledge_fts f
26592
+ CROSS JOIN knowledge k ON k.rowid = f.rowid
26418
26593
  WHERE knowledge_fts MATCH ?
26419
26594
  AND k.project_id IS NOT NULL
26420
26595
  AND k.project_id != ?
@@ -26547,8 +26722,8 @@ function check2(projectPath) {
26547
26722
  try {
26548
26723
  const { title, content: content3, category } = config2().search.ftsWeights;
26549
26724
  const matches = db().query(
26550
- `SELECT k.id, k.title FROM knowledge k
26551
- JOIN knowledge_fts f ON k.rowid = f.rowid
26725
+ `SELECT k.id, k.title FROM knowledge_fts f
26726
+ CROSS JOIN knowledge k ON k.rowid = f.rowid
26552
26727
  WHERE knowledge_fts MATCH ?
26553
26728
  AND k.id != ?
26554
26729
  AND k.confidence > 0.2
@@ -26582,9 +26757,13 @@ function check2(projectPath) {
26582
26757
  // src/distillation.ts
26583
26758
  var distillation_exports = {};
26584
26759
  __export(distillation_exports, {
26760
+ latestMetaObservations: () => latestMetaObservations,
26585
26761
  loadForSession: () => loadForSession,
26762
+ messagesToText: () => messagesToText,
26763
+ metaDistill: () => metaDistill,
26586
26764
  parseSourceIds: () => parseSourceIds,
26587
26765
  run: () => run,
26766
+ truncateToolOutputsInContent: () => truncateToolOutputsInContent,
26588
26767
  workerSessionIDs: () => workerSessionIDs
26589
26768
  });
26590
26769
 
@@ -26609,6 +26788,8 @@ function estimateMessage(msg) {
26609
26788
  }
26610
26789
  var contextLimit = 2e5;
26611
26790
  var outputReserved = 32e3;
26791
+ var maxLayer0Tokens = 0;
26792
+ var MIN_LAYER0_FLOOR = 4e4;
26612
26793
  var FIRST_TURN_OVERHEAD = 15e3;
26613
26794
  var calibratedOverhead = null;
26614
26795
  function makeSessionState() {
@@ -26622,7 +26803,11 @@ function makeSessionState() {
26622
26803
  forceMinLayer: 0,
26623
26804
  lastTransformEstimate: 0,
26624
26805
  prefixCache: null,
26625
- rawWindowCache: null
26806
+ rawWindowCache: null,
26807
+ lastTurnAt: 0,
26808
+ cameOutOfIdle: false,
26809
+ consecutiveHighLayer: 0,
26810
+ lastPrefixHash: ""
26626
26811
  };
26627
26812
  }
26628
26813
  var sessionStates = /* @__PURE__ */ new Map();
@@ -26635,11 +26820,36 @@ function getSessionState(sessionID) {
26635
26820
  }
26636
26821
  return state;
26637
26822
  }
26823
+ function onIdleResume(sessionID, thresholdMs, now = Date.now()) {
26824
+ if (thresholdMs <= 0) return { triggered: false };
26825
+ const state = getSessionState(sessionID);
26826
+ if (state.lastTurnAt === 0) return { triggered: false };
26827
+ const idleMs = now - state.lastTurnAt;
26828
+ if (idleMs < thresholdMs) return { triggered: false };
26829
+ state.prefixCache = null;
26830
+ state.rawWindowCache = null;
26831
+ state.cameOutOfIdle = true;
26832
+ return { triggered: true, idleMs };
26833
+ }
26834
+ function consumeCameOutOfIdle(sessionID) {
26835
+ const state = sessionStates.get(sessionID);
26836
+ if (!state || !state.cameOutOfIdle) return false;
26837
+ state.cameOutOfIdle = false;
26838
+ return true;
26839
+ }
26638
26840
  var ltmTokens = 0;
26639
26841
  function setModelLimits(limits) {
26640
26842
  contextLimit = limits.context || 2e5;
26641
26843
  outputReserved = Math.min(limits.output || 32e3, 32e3);
26642
26844
  }
26845
+ function setMaxLayer0Tokens(tokens) {
26846
+ maxLayer0Tokens = Math.max(0, Math.floor(tokens));
26847
+ }
26848
+ function computeLayer0Cap(targetCostPerTurn, cacheReadCostPerToken) {
26849
+ if (targetCostPerTurn <= 0 || cacheReadCostPerToken <= 0) return 0;
26850
+ const rawCap = Math.floor(targetCostPerTurn / cacheReadCostPerToken);
26851
+ return Math.max(rawCap, MIN_LAYER0_FLOOR);
26852
+ }
26643
26853
  function setLtmTokens(tokens) {
26644
26854
  ltmTokens = tokens;
26645
26855
  }
@@ -26684,6 +26894,19 @@ function setForceMinLayer(layer, sessionID) {
26684
26894
  }
26685
26895
  }
26686
26896
  }
26897
+ function inspectSessionState(sessionID) {
26898
+ const state = sessionStates.get(sessionID);
26899
+ if (!state) return null;
26900
+ return {
26901
+ hasPrefixCache: state.prefixCache !== null,
26902
+ hasRawWindowCache: state.rawWindowCache !== null,
26903
+ cameOutOfIdle: state.cameOutOfIdle,
26904
+ lastTurnAt: state.lastTurnAt
26905
+ };
26906
+ }
26907
+ function setLastTurnAtForTest(sessionID, ms) {
26908
+ getSessionState(sessionID).lastTurnAt = ms;
26909
+ }
26687
26910
  function loadDistillations(projectPath, sessionID) {
26688
26911
  const pid = ensureProject(projectPath);
26689
26912
  const query = sessionID ? "SELECT id, observations, generation, token_count, created_at, session_id FROM distillations WHERE project_id = ? AND session_id = ? AND archived = 0 ORDER BY created_at ASC" : "SELECT id, observations, generation, token_count, created_at, session_id FROM distillations WHERE project_id = ? AND archived = 0 ORDER BY created_at ASC";
@@ -26716,12 +26939,17 @@ function cleanParts(parts) {
26716
26939
  }
26717
26940
  return filtered.length > 0 ? filtered : parts;
26718
26941
  }
26942
+ var ANNOTATION_PATH_SCAN_LIMIT = 64 * 1024;
26943
+ var PATH_RE = /(?:[\w.-]+\/)+[\w.-]+\.\w{1,5}/g;
26719
26944
  function toolStripAnnotation(toolName, output) {
26720
26945
  const lines = output.split("\n").length;
26721
- const chars = output.length;
26722
26946
  const hasError = /\b(?:error|fail(?:ed|ure)?|exception|panic|traceback)\b/i.test(output);
26723
- const paths = output.match(/(?:[\w.-]+\/)+[\w.-]+\.\w{1,5}/g);
26724
- const uniquePaths = paths ? [...new Set(paths)].slice(0, 5) : [];
26947
+ let uniquePaths = [];
26948
+ if (output.indexOf("/") !== -1) {
26949
+ const pathScan = output.length > ANNOTATION_PATH_SCAN_LIMIT ? output.slice(0, ANNOTATION_PATH_SCAN_LIMIT) : output;
26950
+ const paths = pathScan.match(PATH_RE);
26951
+ if (paths) uniquePaths = [...new Set(paths)].slice(0, 5);
26952
+ }
26725
26953
  let annotation = `[output omitted \u2014 ${toolName}: ${lines} lines`;
26726
26954
  if (hasError) annotation += ", contained errors";
26727
26955
  if (uniquePaths.length > 0) annotation += `, paths: ${uniquePaths.join(", ")}`;
@@ -27152,7 +27380,11 @@ function transformInner(input) {
27152
27380
  expectedInput = messageTokens + overhead + ltmTokens;
27153
27381
  }
27154
27382
  const layer0Input = calibrated ? expectedInput : expectedInput * UNCALIBRATED_SAFETY;
27155
- if (effectiveMinLayer === 0 && layer0Input <= maxInput) {
27383
+ let layer0Ceiling = maxLayer0Tokens > 0 ? Math.min(maxInput, maxLayer0Tokens) : maxInput;
27384
+ if (!calibrated && layer0Ceiling < maxInput) {
27385
+ layer0Ceiling = Math.floor(layer0Ceiling * 0.7);
27386
+ }
27387
+ if (effectiveMinLayer === 0 && layer0Input <= layer0Ceiling) {
27156
27388
  const messageTokens = calibrated ? expectedInput - (ltmTokens - sessState.lastKnownLtm) : expectedInput - overhead - ltmTokens;
27157
27389
  return {
27158
27390
  messages: input.messages,
@@ -27233,14 +27465,31 @@ function transformInner(input) {
27233
27465
  (sum, m) => sum + estimateMessage(m),
27234
27466
  0
27235
27467
  );
27236
- const nuclearRaw = input.messages.slice(-3).map((m) => ({
27468
+ const tailBudget = Math.max(2e3, Math.min(8e3, Math.floor(usable * 0.25)));
27469
+ const nuclearTurnStart = currentTurnStart(input.messages);
27470
+ const currentTurn = input.messages.slice(nuclearTurnStart).map((m) => ({
27237
27471
  info: m.info,
27238
27472
  parts: cleanParts(m.parts)
27239
27473
  }));
27240
- const nuclearRawTokens = nuclearRaw.reduce(
27474
+ const currentTurnTokens = currentTurn.reduce(
27241
27475
  (sum, m) => sum + estimateMessage(m),
27242
27476
  0
27243
27477
  );
27478
+ const olderMessages = [];
27479
+ let olderTokens = 0;
27480
+ const remaining = Math.max(0, tailBudget - currentTurnTokens);
27481
+ for (let i = nuclearTurnStart - 1; i >= 0 && olderTokens < remaining; i--) {
27482
+ const msg = input.messages[i];
27483
+ const est = estimateMessage(msg);
27484
+ if (olderTokens + est > remaining) break;
27485
+ olderMessages.unshift({
27486
+ info: msg.info,
27487
+ parts: cleanParts(msg.parts)
27488
+ });
27489
+ olderTokens += est;
27490
+ }
27491
+ const nuclearRaw = [...olderMessages, ...currentTurn];
27492
+ const nuclearRawTokens = olderTokens + currentTurnTokens;
27244
27493
  return {
27245
27494
  messages: [...nuclearPrefix, ...nuclearRaw],
27246
27495
  layer: 4,
@@ -27262,19 +27511,55 @@ function transform2(input) {
27262
27511
  state.lastTransformEstimate = result.totalTokens;
27263
27512
  state.lastLayer = result.layer;
27264
27513
  state.lastWindowMessageIDs = new Set(result.messages.map((m) => m.info.id));
27514
+ state.lastTurnAt = Date.now();
27515
+ const prefixIds = result.messages.slice(0, 5).map((m) => m.info.id).join(",");
27516
+ const prefixHash = `${result.layer}:${prefixIds}`;
27517
+ if (state.lastPrefixHash && state.lastPrefixHash !== prefixHash) {
27518
+ info(
27519
+ `cache-bust detected: session=${sid} layer=${state.lastLayer}\u2192${result.layer} msgs=${state.lastTransformedCount}\u2192${result.messages.length} prefix=${state.lastPrefixHash.slice(0, 30)}\u2192${prefixHash.slice(0, 30)}`
27520
+ );
27521
+ }
27522
+ state.lastPrefixHash = prefixHash;
27523
+ if (result.layer >= 2) {
27524
+ state.consecutiveHighLayer++;
27525
+ if (state.consecutiveHighLayer === 3) {
27526
+ info(
27527
+ `session ${sid} has been at gradient layer ${result.layer}+ for 3 consecutive turns. Consider running /compact to reset the context window.`
27528
+ );
27529
+ }
27530
+ } else {
27531
+ state.consecutiveHighLayer = 0;
27532
+ }
27533
+ info(
27534
+ `gradient: session=${sid} layer=${result.layer} tokens=${result.totalTokens} (distilled=${result.distilledTokens} raw=${result.rawTokens}) usable=${result.usable} cap=${maxLayer0Tokens || "off"}`
27535
+ );
27265
27536
  }
27266
27537
  return result;
27267
27538
  }
27268
27539
  function currentTurnStart(messages) {
27269
- let lastUserIdx = -1;
27540
+ if (messages.length === 0) return 0;
27541
+ let boundary = messages.length;
27270
27542
  for (let i = messages.length - 1; i >= 0; i--) {
27271
27543
  if (messages[i].info.role === "user") {
27272
- lastUserIdx = i;
27544
+ boundary = i;
27273
27545
  break;
27274
27546
  }
27275
27547
  }
27276
- if (lastUserIdx === -1) return 0;
27277
- return lastUserIdx;
27548
+ if (boundary === messages.length) return 0;
27549
+ for (let i = boundary - 1; i >= 0; i--) {
27550
+ const msg = messages[i];
27551
+ const hasToolParts = msg.parts.some(isToolPart);
27552
+ if (hasToolParts) {
27553
+ boundary = i;
27554
+ continue;
27555
+ }
27556
+ if (msg.info.role === "user") {
27557
+ boundary = i;
27558
+ continue;
27559
+ }
27560
+ break;
27561
+ }
27562
+ return boundary;
27278
27563
  }
27279
27564
  function tryFit(input) {
27280
27565
  if (input.prefixTokens > input.distilledBudget && input.prefix.length > 0)
@@ -27355,8 +27640,39 @@ function formatTime(ms) {
27355
27640
  const m = d.getMinutes().toString().padStart(2, "0");
27356
27641
  return `${h3}:${m}`;
27357
27642
  }
27358
- function messagesToText(messages) {
27359
- return messages.map((m) => `[${m.role}] (${formatTime(m.created_at)}) ${m.content}`).join("\n\n");
27643
+ var CHUNK_SEPARATOR = "\n" + CHUNK_TERMINATOR;
27644
+ function truncateToolOutputsInContent(content3, maxChars) {
27645
+ if (maxChars <= 0 || content3.length === 0) return content3;
27646
+ if (content3.indexOf(CHUNK_TERMINATOR) === -1) {
27647
+ return truncateSingleChunk(content3, maxChars);
27648
+ }
27649
+ const chunks = content3.split(CHUNK_SEPARATOR);
27650
+ let anyToolChunk = false;
27651
+ for (const c of chunks) {
27652
+ if (c.startsWith("[tool:")) {
27653
+ anyToolChunk = true;
27654
+ break;
27655
+ }
27656
+ }
27657
+ if (!anyToolChunk) return content3;
27658
+ const out = chunks.map((chunk) => truncateSingleChunk(chunk, maxChars));
27659
+ return out.join(CHUNK_SEPARATOR);
27660
+ }
27661
+ function truncateSingleChunk(chunk, maxChars) {
27662
+ if (!chunk.startsWith("[tool:")) return chunk;
27663
+ const closeBracket = chunk.indexOf("] ");
27664
+ if (closeBracket < 0) return chunk;
27665
+ const toolName = chunk.slice(6, closeBracket);
27666
+ const payload = chunk.slice(closeBracket + 2);
27667
+ if (payload.length <= maxChars) return chunk;
27668
+ return `[tool:${toolName}] ${toolStripAnnotation(toolName, payload)}`;
27669
+ }
27670
+ function messagesToText(messages, toolOutputMaxChars) {
27671
+ const cap = toolOutputMaxChars ?? config2().distillation.toolOutputMaxChars;
27672
+ return messages.map((m) => {
27673
+ const body = m.role === "user" ? m.content : truncateToolOutputsInContent(m.content, cap);
27674
+ return `[${m.role}] (${formatTime(m.created_at)}) ${body}`;
27675
+ }).join("\n\n");
27360
27676
  }
27361
27677
  function parseDistillationResult(text4) {
27362
27678
  const match = text4.match(/<observations>([\s\S]*?)<\/observations>/i);
@@ -27371,6 +27687,19 @@ function latestObservations(projectPath, sessionID) {
27371
27687
  ).get(pid, sessionID);
27372
27688
  return row?.observations || void 0;
27373
27689
  }
27690
+ function latestMetaObservations(projectPath, sessionID) {
27691
+ return latestMeta(projectPath, sessionID)?.observations;
27692
+ }
27693
+ function latestMeta(projectPath, sessionID) {
27694
+ const pid = ensureProject(projectPath);
27695
+ const row = db().query(
27696
+ `SELECT observations, generation FROM distillations
27697
+ WHERE project_id = ? AND session_id = ? AND generation > 0
27698
+ ORDER BY generation DESC, created_at DESC LIMIT 1`
27699
+ ).get(pid, sessionID);
27700
+ if (!row || !row.observations) return void 0;
27701
+ return row;
27702
+ }
27374
27703
  function parseSourceIds(raw) {
27375
27704
  try {
27376
27705
  const parsed = JSON.parse(raw);
@@ -27380,11 +27709,10 @@ function parseSourceIds(raw) {
27380
27709
  return [];
27381
27710
  }
27382
27711
  }
27383
- function loadForSession(projectPath, sessionID) {
27712
+ function loadForSession(projectPath, sessionID, includeArchived = false) {
27384
27713
  const pid = ensureProject(projectPath);
27385
- const rows = db().query(
27386
- "SELECT id, project_id, session_id, observations, source_ids, generation, token_count, created_at FROM distillations WHERE project_id = ? AND session_id = ? ORDER BY created_at ASC"
27387
- ).all(pid, sessionID);
27714
+ const sql = includeArchived ? "SELECT id, project_id, session_id, observations, source_ids, generation, token_count, created_at FROM distillations WHERE project_id = ? AND session_id = ? ORDER BY created_at ASC" : "SELECT id, project_id, session_id, observations, source_ids, generation, token_count, created_at FROM distillations WHERE project_id = ? AND session_id = ? AND archived = 0 ORDER BY created_at ASC";
27715
+ const rows = db().query(sql).all(pid, sessionID);
27388
27716
  return rows.map((r) => ({
27389
27717
  ...r,
27390
27718
  source_ids: parseSourceIds(r.source_ids)
@@ -27549,8 +27877,13 @@ async function distillSegment(input) {
27549
27877
  }
27550
27878
  async function metaDistill(input) {
27551
27879
  const existing = loadGen0(input.projectPath, input.sessionID);
27552
- if (existing.length < 3) return null;
27553
- const userContent = recursiveUser(existing);
27880
+ const priorMeta = latestMeta(input.projectPath, input.sessionID);
27881
+ if (priorMeta) {
27882
+ if (existing.length === 0) return null;
27883
+ } else {
27884
+ if (existing.length < 3) return null;
27885
+ }
27886
+ const userContent = recursiveUser(existing, priorMeta?.observations);
27554
27887
  const model = input.model ?? config2().model;
27555
27888
  const responseText = await input.llm.prompt(
27556
27889
  RECURSIVE_SYSTEM,
@@ -27560,19 +27893,30 @@ async function metaDistill(input) {
27560
27893
  if (!responseText) return null;
27561
27894
  const result = parseDistillationResult(responseText);
27562
27895
  if (!result) return null;
27563
- const maxGen = Math.max(...existing.map((d) => d.generation));
27896
+ const maxGen = Math.max(
27897
+ ...existing.map((d) => d.generation),
27898
+ priorMeta?.generation ?? 0
27899
+ );
27564
27900
  const allSourceIDs = existing.flatMap((d) => d.source_ids);
27565
- const metaId = storeDistillation({
27566
- projectPath: input.projectPath,
27567
- sessionID: input.sessionID,
27568
- observations: result.observations,
27569
- sourceIDs: allSourceIDs,
27570
- generation: maxGen + 1
27571
- });
27901
+ let metaId;
27902
+ db().exec("BEGIN IMMEDIATE");
27903
+ try {
27904
+ metaId = storeDistillation({
27905
+ projectPath: input.projectPath,
27906
+ sessionID: input.sessionID,
27907
+ observations: result.observations,
27908
+ sourceIDs: allSourceIDs,
27909
+ generation: maxGen + 1
27910
+ });
27911
+ archiveDistillations(existing.map((d) => d.id));
27912
+ db().exec("COMMIT");
27913
+ } catch (e) {
27914
+ db().exec("ROLLBACK");
27915
+ throw e;
27916
+ }
27572
27917
  if (isAvailable()) {
27573
27918
  embedDistillation(metaId, result.observations);
27574
27919
  }
27575
- archiveDistillations(existing.map((d) => d.id));
27576
27920
  return result;
27577
27921
  }
27578
27922
 
@@ -27731,13 +28075,13 @@ function searchDistillationsScored(input) {
27731
28075
  const q = ftsQuery(input.query);
27732
28076
  if (q === EMPTY_QUERY) return [];
27733
28077
  const ftsSQL = input.sessionID ? `SELECT d.id, d.observations, d.generation, d.created_at, d.session_id, rank
27734
- FROM distillations d
27735
- JOIN distillation_fts f ON d.rowid = f.rowid
28078
+ FROM distillation_fts f
28079
+ CROSS JOIN distillations d ON d.rowid = f.rowid
27736
28080
  WHERE distillation_fts MATCH ?
27737
28081
  AND d.project_id = ? AND d.session_id = ?
27738
28082
  ORDER BY rank LIMIT ?` : `SELECT d.id, d.observations, d.generation, d.created_at, d.session_id, rank
27739
- FROM distillations d
27740
- JOIN distillation_fts f ON d.rowid = f.rowid
28083
+ FROM distillation_fts f
28084
+ CROSS JOIN distillations d ON d.rowid = f.rowid
27741
28085
  WHERE distillation_fts MATCH ?
27742
28086
  AND d.project_id = ?
27743
28087
  ORDER BY rank LIMIT ?`;
@@ -28180,7 +28524,130 @@ function importFromFile(input) {
28180
28524
  }
28181
28525
  }
28182
28526
  }
28527
+
28528
+ // src/worker-model.ts
28529
+ var worker_model_exports = {};
28530
+ __export(worker_model_exports, {
28531
+ WORKER_JUDGE_SYSTEM: () => WORKER_JUDGE_SYSTEM,
28532
+ computeModelFingerprint: () => computeModelFingerprint,
28533
+ getValidatedWorkerModel: () => getValidatedWorkerModel,
28534
+ isValidationStale: () => isValidationStale,
28535
+ parseJudgeScore: () => parseJudgeScore,
28536
+ resolveWorkerModel: () => resolveWorkerModel,
28537
+ selectWorkerCandidates: () => selectWorkerCandidates,
28538
+ storeValidatedWorkerModel: () => storeValidatedWorkerModel,
28539
+ structuralCheck: () => structuralCheck,
28540
+ workerJudgeUser: () => workerJudgeUser
28541
+ });
28542
+ var KV_PREFIX = "lore:worker_model:";
28543
+ function selectWorkerCandidates(sessionModel, providerModels) {
28544
+ const eligible = providerModels.filter(
28545
+ (m) => m.providerID === sessionModel.providerID && m.status === "active" && m.capabilities.input.text
28546
+ );
28547
+ if (eligible.length === 0) return [];
28548
+ const sorted = [...eligible].sort((a, b) => a.cost.input - b.cost.input);
28549
+ const cheapest = sorted[0];
28550
+ const belowSession = sorted.filter((m) => m.cost.input < sessionModel.cost.input).pop();
28551
+ const candidates = /* @__PURE__ */ new Map();
28552
+ candidates.set(cheapest.id, cheapest);
28553
+ if (belowSession && belowSession.id !== cheapest.id) {
28554
+ candidates.set(belowSession.id, belowSession);
28555
+ }
28556
+ if (cheapest.id === sessionModel.id || cheapest.cost.input >= sessionModel.cost.input) {
28557
+ return [cheapest];
28558
+ }
28559
+ return [...candidates.values()];
28560
+ }
28561
+ function computeModelFingerprint(providerID, sessionModelID, activeModelIDs) {
28562
+ const sorted = [...activeModelIDs].sort();
28563
+ return sha256(
28564
+ JSON.stringify({ providerID, sessionModelID, modelIDs: sorted })
28565
+ );
28566
+ }
28567
+ function getValidatedWorkerModel(providerID) {
28568
+ const row = db().query("SELECT value FROM kv_meta WHERE key = ?").get(`${KV_PREFIX}${providerID}`);
28569
+ if (!row) return null;
28570
+ try {
28571
+ return JSON.parse(row.value);
28572
+ } catch {
28573
+ return null;
28574
+ }
28575
+ }
28576
+ function storeValidatedWorkerModel(result) {
28577
+ const key = `${KV_PREFIX}${result.providerID}`;
28578
+ const value = JSON.stringify(result);
28579
+ db().query(
28580
+ "INSERT INTO kv_meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = ?"
28581
+ ).run(key, value, value);
28582
+ }
28583
+ function isValidationStale(stored, currentFingerprint) {
28584
+ if (!stored) return true;
28585
+ return stored.fingerprint !== currentFingerprint;
28586
+ }
28587
+ function structuralCheck(candidateObservations, referenceObservations) {
28588
+ if (candidateObservations == null || candidateObservations.length === 0) {
28589
+ return { passed: false, observationCount: 0, tokenCount: 0, reason: candidateObservations === null ? "parse_failed" : "empty" };
28590
+ }
28591
+ const countObs = (text4) => text4.split("\n").filter((l) => l.trim().length > 0).length;
28592
+ const refCount = countObs(referenceObservations);
28593
+ const candCount = countObs(candidateObservations);
28594
+ const candTokens = Math.ceil(candidateObservations.length / 3);
28595
+ if (refCount > 0 && (candCount < refCount * 0.5 || candCount > refCount * 1.5)) {
28596
+ return {
28597
+ passed: false,
28598
+ observationCount: candCount,
28599
+ tokenCount: candTokens,
28600
+ reason: `observation_count_${candCount}_vs_ref_${refCount}`
28601
+ };
28602
+ }
28603
+ const refTokens = Math.ceil(referenceObservations.length / 3);
28604
+ if (candTokens === 0) {
28605
+ return { passed: false, observationCount: candCount, tokenCount: candTokens, reason: "empty" };
28606
+ }
28607
+ if (refTokens > 0 && candTokens > refTokens * 3) {
28608
+ return {
28609
+ passed: false,
28610
+ observationCount: candCount,
28611
+ tokenCount: candTokens,
28612
+ reason: `token_count_${candTokens}_vs_ref_${refTokens}_3x`
28613
+ };
28614
+ }
28615
+ return { passed: true, observationCount: candCount, tokenCount: candTokens };
28616
+ }
28617
+ var WORKER_JUDGE_SYSTEM = `You are evaluating distillation quality. You will be given a REFERENCE distillation (produced by a capable model) and a CANDIDATE distillation (produced by a cheaper model) of the same conversation segment.
28618
+
28619
+ Rate the candidate on a scale of 1-5:
28620
+ 5 = Captures all key facts and decisions, equivalent to reference
28621
+ 4 = Captures most facts, minor omissions
28622
+ 3 = Captures the essential facts, some detail loss acceptable
28623
+ 2 = Missing important facts or technical details
28624
+ 1 = Significantly incomplete or inaccurate
28625
+
28626
+ Respond with ONLY a single digit (1-5).`;
28627
+ function workerJudgeUser(reference, candidate) {
28628
+ return `<reference>
28629
+ ${reference}
28630
+ </reference>
28631
+
28632
+ <candidate>
28633
+ ${candidate}
28634
+ </candidate>`;
28635
+ }
28636
+ function parseJudgeScore(response) {
28637
+ const match = response.trim().match(/^([1-5])/);
28638
+ if (!match) return null;
28639
+ return parseInt(match[1], 10);
28640
+ }
28641
+ function resolveWorkerModel(providerID, configWorkerModel, configModel) {
28642
+ if (configWorkerModel) return configWorkerModel;
28643
+ const validated = getValidatedWorkerModel(providerID);
28644
+ if (validated) {
28645
+ return { providerID: validated.providerID, modelID: validated.modelID };
28646
+ }
28647
+ return configModel;
28648
+ }
28183
28649
  export {
28650
+ COMPACT_SUMMARY_TEMPLATE,
28184
28651
  CONSOLIDATION_SYSTEM,
28185
28652
  CURATOR_SYSTEM,
28186
28653
  DISTILLATION_SYSTEM,
@@ -28189,10 +28656,14 @@ export {
28189
28656
  RECALL_PARAM_DESCRIPTIONS,
28190
28657
  RECALL_TOOL_DESCRIPTION,
28191
28658
  RECURSIVE_SYSTEM,
28659
+ WORKER_JUDGE_SYSTEM,
28660
+ buildCompactPrompt,
28192
28661
  calibrate,
28193
28662
  close,
28663
+ computeLayer0Cap,
28194
28664
  config2 as config,
28195
28665
  consolidationUser,
28666
+ consumeCameOutOfIdle,
28196
28667
  curator_exports as curator,
28197
28668
  curatorUser,
28198
28669
  db,
@@ -28214,6 +28685,7 @@ export {
28214
28685
  h,
28215
28686
  importFromFile,
28216
28687
  inline,
28688
+ inspectSessionState,
28217
28689
  isFirstRun,
28218
28690
  isReasoningPart,
28219
28691
  isTextPart,
@@ -28228,6 +28700,7 @@ export {
28228
28700
  ltm_exports as ltm,
28229
28701
  needsUrgentDistillation,
28230
28702
  normalize,
28703
+ onIdleResume,
28231
28704
  p,
28232
28705
  projectId,
28233
28706
  projectName,
@@ -28239,15 +28712,20 @@ export {
28239
28712
  saveForceMinLayer,
28240
28713
  serialize,
28241
28714
  setForceMinLayer,
28715
+ setLastTurnAtForTest,
28242
28716
  setLtmTokens,
28717
+ setMaxLayer0Tokens,
28243
28718
  setModelLimits,
28244
28719
  shouldImport,
28245
28720
  strong2 as strong,
28246
28721
  t,
28247
28722
  temporal_exports as temporal,
28723
+ toolStripAnnotation,
28248
28724
  transform2 as transform,
28249
28725
  ul,
28250
28726
  unescapeMarkdown,
28727
+ workerJudgeUser,
28728
+ worker_model_exports as workerModel,
28251
28729
  workerSessionIDs
28252
28730
  };
28253
28731
  //# sourceMappingURL=index.js.map