@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
package/dist/bun/index.js CHANGED
@@ -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,
@@ -419,6 +421,58 @@ var MIGRATIONS = [
419
421
  to_id TEXT NOT NULL REFERENCES knowledge(id) ON DELETE CASCADE,
420
422
  PRIMARY KEY (from_id, to_id)
421
423
  );
424
+ `,
425
+ `
426
+ -- Version 11: F3b -- unambiguous chunk terminator in temporal_messages.content.
427
+ --
428
+ -- Pre-F3b, partsToText joined chunks with a newline. Tool-output payloads
429
+ -- can contain newlines too, so the boundary between a tool envelope and a
430
+ -- following plain-text or [reasoning] chunk was structurally ambiguous.
431
+ -- This caused two known limitations in the F3 distill-input truncator:
432
+ -- trailing text could be swallowed into a tool payload, and embedded
433
+ -- literal envelope strings inside a payload (e.g. when reading AGENTS.md)
434
+ -- could fabricate fake boundaries.
435
+ --
436
+ -- F3b switches the chunk separator to newline plus ASCII Unit Separator
437
+ -- (char 31). The Unit Separator is non-word so FTS5's unicode61 tokenizer
438
+ -- ignores it (zero BM25 impact). New rows are written via the post-F3b
439
+ -- partsToText. Existing rows are rewritten in place by the UPDATE below,
440
+ -- which uses pure SQL replace() to inject the Unit Separator after every
441
+ -- legacy chunk-prefix sequence -- the same boundary patterns the legacy
442
+ -- F3 reader was already trying to recover.
443
+ --
444
+ -- Trade-off (acceptable): any embedded legacy chunk-prefix sequence
445
+ -- inside a tool payload becomes a structural boundary post-migration.
446
+ -- This matches what the legacy F3 reader did at read-time anyway, baked
447
+ -- into the row permanently. The migration runs once per machine.
448
+ --
449
+ -- Idempotent: a row that already contains the Unit Separator before a
450
+ -- chunk prefix no longer matches the search literal (the separator
451
+ -- interposes), so re-running the UPDATE is a no-op for migrated rows.
452
+ -- (Important: migrate() in db.ts runs each migration via database.exec()
453
+ -- with no explicit BEGIN/COMMIT around the whole loop. SQLite makes this
454
+ -- single UPDATE statement atomic per-statement, so partial progress on
455
+ -- crash is safe to retry thanks to the idempotency above.)
456
+ --
457
+ -- char(10) = newline, char(31) = Unit Separator. SQLite has no native
458
+ -- regex, but two nested replace() calls on the literal prefixes are
459
+ -- sufficient because both legacy chunk prefixes match at line-start.
460
+ --
461
+ -- Each row UPDATE fires the temporal_fts_update trigger once; because
462
+ -- the Unit Separator is a non-word character, the re-indexed content
463
+ -- tokenizes identically -- net no-op for FTS scoring.
464
+ UPDATE temporal_messages
465
+ SET content = replace(
466
+ replace(
467
+ content,
468
+ char(10) || '[tool:',
469
+ char(10) || char(31) || '[tool:'
470
+ ),
471
+ char(10) || '[reasoning] ',
472
+ char(10) || char(31) || '[reasoning] '
473
+ )
474
+ WHERE content LIKE '%' || char(10) || '[tool:%'
475
+ OR content LIKE '%' || char(10) || '[reasoning] %';
422
476
  `
423
477
  ];
424
478
  function dataDir() {
@@ -439,11 +493,13 @@ function db() {
439
493
  mkdirSync(dir, { recursive: true });
440
494
  path = join(dir, "lore.db");
441
495
  }
442
- instance = new Database(path);
443
- instance.exec("PRAGMA journal_mode = WAL");
444
- instance.exec("PRAGMA foreign_keys = ON");
445
- instance.exec("PRAGMA auto_vacuum = INCREMENTAL");
446
- migrate(instance);
496
+ const database = new Database(path);
497
+ database.exec("PRAGMA journal_mode = WAL");
498
+ database.exec("PRAGMA foreign_keys = ON");
499
+ database.exec("PRAGMA busy_timeout = 5000");
500
+ database.exec("PRAGMA auto_vacuum = INCREMENTAL");
501
+ migrate(database);
502
+ instance = database;
447
503
  return instance;
448
504
  }
449
505
  var VACUUM_MIGRATION_INDEX = 2;
@@ -10748,12 +10804,27 @@ EXACT NUMBERS: When two segments report different numbers for what seems like th
10748
10804
 
10749
10805
  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.
10750
10806
 
10807
+ 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.
10808
+
10751
10809
  Output ONLY an <observations> block with the consolidated observations.`;
10752
- function recursiveUser(distillations) {
10810
+ function recursiveUser(distillations, previousMeta) {
10753
10811
  const entries = distillations.map(
10754
10812
  (d, i) => `Segment ${i + 1}:
10755
10813
  ${d.observations}`
10756
10814
  );
10815
+ if (previousMeta) {
10816
+ 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.
10817
+
10818
+ <previous-meta-summary>
10819
+ ${previousMeta}
10820
+ </previous-meta-summary>
10821
+
10822
+ ---
10823
+
10824
+ New observation segments to merge (chronological order):
10825
+
10826
+ ${entries.join("\n\n---\n\n")}`;
10827
+ }
10757
10828
  return `Observation segments to consolidate (chronological order):
10758
10829
 
10759
10830
  ${entries.join("\n\n---\n\n")}`;
@@ -10908,6 +10979,61 @@ function formatDistillations(distillations) {
10908
10979
  }
10909
10980
  return sections.join("\n\n");
10910
10981
  }
10982
+ var COMPACT_SUMMARY_TEMPLATE = `Output exactly this Markdown structure. Keep every section in this order, even when empty (use "(none)").
10983
+
10984
+ ---
10985
+ ## Goal
10986
+ - [single-sentence task summary]
10987
+
10988
+ ## Constraints & Preferences
10989
+ - [user constraints, preferences, specs, or "(none)"]
10990
+
10991
+ ## Progress
10992
+ ### Done
10993
+ - [completed work or "(none)"]
10994
+
10995
+ ### In Progress
10996
+ - [current work or "(none)"]
10997
+
10998
+ ### Blocked
10999
+ - [blockers or "(none)"]
11000
+
11001
+ ## Key Decisions
11002
+ - [decision and why, or "(none)"]
11003
+
11004
+ ## Next Steps
11005
+ - [ordered next actions or "(none)"]
11006
+
11007
+ ## Critical Context
11008
+ - [important technical facts, errors, open questions, or "(none)"]
11009
+
11010
+ ## Relevant Files
11011
+ - [file or directory path: why it matters, or "(none)"]
11012
+ ---
11013
+
11014
+ Rules:
11015
+ - Keep every section, even when empty.
11016
+ - Use terse bullets, not prose paragraphs.
11017
+ - Preserve exact file paths, commands, error strings, and identifiers when known.
11018
+ - Do not mention the summary process or that context was compacted.
11019
+ - End with "I'm ready to continue." on its own line after the closing "---".`;
11020
+ function buildCompactPrompt(input) {
11021
+ 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" : "";
11022
+ 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.
11023
+
11024
+ <previous-summary>
11025
+ ${input.previousSummary}
11026
+ </previous-summary>
11027
+
11028
+ ` : "";
11029
+ const knowledgeBlock = input.knowledge ? `
11030
+ ${input.knowledge}
11031
+ ` : "";
11032
+ 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.
11033
+
11034
+ ${distillSection}${anchorBlock}${COMPACT_SUMMARY_TEMPLATE}
11035
+ ${knowledgeBlock}`;
11036
+ }
10911
11037
  function estimateTokens(text4) {
10912
11038
  return Math.ceil(text4.length / 3);
10913
11039
  }
@@ -11160,6 +11286,7 @@ function isToolPart(p3) {
11160
11286
  function estimate(text4) {
11161
11287
  return Math.ceil(text4.length / 3);
11162
11288
  }
11289
+ var CHUNK_TERMINATOR = "";
11163
11290
  function partsToText(parts) {
11164
11291
  const chunks = [];
11165
11292
  for (const part of parts) {
@@ -11169,7 +11296,7 @@ function partsToText(parts) {
11169
11296
  else if (isToolPart(part) && part.state.status === "completed")
11170
11297
  chunks.push(`[tool:${part.tool}] ${part.state.output}`);
11171
11298
  }
11172
- return sanitizeSurrogates(chunks.join("\n"));
11299
+ return sanitizeSurrogates(chunks.join("\n" + CHUNK_TERMINATOR));
11173
11300
  }
11174
11301
  function messageMetadata(info2, parts) {
11175
11302
  const meta3 = {};
@@ -11248,11 +11375,11 @@ function search2(input) {
11248
11375
  const limit = input.limit ?? 20;
11249
11376
  const q = ftsQuery(input.query);
11250
11377
  if (q === EMPTY_QUERY) return [];
11251
- const ftsSQL = input.sessionID ? `SELECT m.* FROM temporal_messages m
11252
- JOIN temporal_fts f ON m.rowid = f.rowid
11378
+ const ftsSQL = input.sessionID ? `SELECT m.* FROM temporal_fts f
11379
+ CROSS JOIN temporal_messages m ON m.rowid = f.rowid
11253
11380
  WHERE f.content MATCH ? AND m.project_id = ? AND m.session_id = ?
11254
- ORDER BY rank LIMIT ?` : `SELECT m.* FROM temporal_messages m
11255
- JOIN temporal_fts f ON m.rowid = f.rowid
11381
+ ORDER BY rank LIMIT ?` : `SELECT m.* FROM temporal_fts f
11382
+ CROSS JOIN temporal_messages m ON m.rowid = f.rowid
11256
11383
  WHERE f.content MATCH ? AND m.project_id = ?
11257
11384
  ORDER BY rank LIMIT ?`;
11258
11385
  const params = input.sessionID ? [q, pid, input.sessionID, limit] : [q, pid, limit];
@@ -11277,11 +11404,11 @@ function searchScored(input) {
11277
11404
  const limit = input.limit ?? 20;
11278
11405
  const q = ftsQuery(input.query);
11279
11406
  if (q === EMPTY_QUERY) return [];
11280
- const ftsSQL = input.sessionID ? `SELECT m.*, rank FROM temporal_messages m
11281
- JOIN temporal_fts f ON m.rowid = f.rowid
11407
+ const ftsSQL = input.sessionID ? `SELECT m.*, rank FROM temporal_fts f
11408
+ CROSS JOIN temporal_messages m ON m.rowid = f.rowid
11282
11409
  WHERE f.content MATCH ? AND m.project_id = ? AND m.session_id = ?
11283
- ORDER BY rank LIMIT ?` : `SELECT m.*, rank FROM temporal_messages m
11284
- JOIN temporal_fts f ON m.rowid = f.rowid
11410
+ ORDER BY rank LIMIT ?` : `SELECT m.*, rank FROM temporal_fts f
11411
+ CROSS JOIN temporal_messages m ON m.rowid = f.rowid
11285
11412
  WHERE f.content MATCH ? AND m.project_id = ?
11286
11413
  ORDER BY rank LIMIT ?`;
11287
11414
  const params = input.sessionID ? [q, pid, input.sessionID, limit] : [q, pid, limit];
@@ -25483,18 +25610,63 @@ var LoreConfig = external_exports.object({
25483
25610
  providerID: external_exports.string(),
25484
25611
  modelID: external_exports.string()
25485
25612
  }).optional(),
25613
+ /** Explicit worker model override. When set, all background workers (distillation,
25614
+ * curation, query expansion) use this model instead of the session model or the
25615
+ * auto-selected worker model. Bypasses dynamic worker model selection entirely. */
25616
+ workerModel: external_exports.object({
25617
+ providerID: external_exports.string(),
25618
+ modelID: external_exports.string()
25619
+ }).optional(),
25486
25620
  budget: external_exports.object({
25487
25621
  distilled: external_exports.number().min(0.05).max(0.5).default(0.25),
25488
25622
  raw: external_exports.number().min(0.1).max(0.7).default(0.4),
25489
25623
  output: external_exports.number().min(0.1).max(0.5).default(0.25),
25490
- /** Max fraction of usable context reserved for LTM system-prompt injection. Default: 0.10 (10%). */
25491
- ltm: external_exports.number().min(0.02).max(0.3).default(0.1)
25492
- }).default({ distilled: 0.25, raw: 0.4, output: 0.25, ltm: 0.1 }),
25624
+ /** Max fraction of usable context reserved for LTM system-prompt injection. Default: 0.05 (5%). */
25625
+ ltm: external_exports.number().min(0.02).max(0.3).default(0.05),
25626
+ /** Per-turn cache-read cost target in dollars. Controls when layer 0 (full
25627
+ * passthrough) escalates to layer 1 (compressed). The cap is derived as:
25628
+ * maxLayer0Tokens = max(target / model.cost.cache.read, 40K).
25629
+ * Lower = cheaper but earlier compression. Default: 0.10. Set to 0 to
25630
+ * disable cost-aware capping (use the model's full context). */
25631
+ targetCacheReadCostPerTurn: external_exports.number().min(0).default(0.1),
25632
+ /** Direct override for the layer-0 token cap. When set, bypasses the
25633
+ * cost-aware formula from targetCacheReadCostPerTurn. 0 = disabled
25634
+ * (no cap, use full context). Default: undefined (use cost-aware auto). */
25635
+ maxLayer0Tokens: external_exports.number().min(0).optional()
25636
+ }).default({ distilled: 0.25, raw: 0.4, output: 0.25, ltm: 0.05, targetCacheReadCostPerTurn: 0.1 }),
25637
+ /**
25638
+ * Cold-cache idle-resume handling.
25639
+ *
25640
+ * Anthropic's prompt cache evicts entries after ~5 min (default tier) /
25641
+ * ~1 hour (extended tier). When a session resumes after the eviction window,
25642
+ * Lore's byte-identity caches (distilled prefix, raw window pin, LTM block)
25643
+ * are providing no value because the underlying provider cache is already
25644
+ * cold. On detection, Lore refreshes those caches so the next turn can
25645
+ * produce a better-fitting window without paying a cache cost it would
25646
+ * otherwise be trying to preserve. Reasoning blocks are NOT touched —
25647
+ * Anthropic's April 23 postmortem identified dropping reasoning blocks as
25648
+ * the root cause of forgetfulness/repetition.
25649
+ *
25650
+ * `idleResumeMinutes` is the threshold in minutes. Default 60 — matches
25651
+ * Anthropic's extended-cache eviction window, conservative across providers.
25652
+ * Set to 0 to disable the feature.
25653
+ */
25654
+ idleResumeMinutes: external_exports.number().min(0).max(24 * 60).default(60),
25493
25655
  distillation: external_exports.object({
25494
- minMessages: external_exports.number().min(3).default(8),
25495
- maxSegment: external_exports.number().min(5).default(50),
25496
- metaThreshold: external_exports.number().min(3).default(10)
25497
- }).default({ minMessages: 8, maxSegment: 50, metaThreshold: 10 }),
25656
+ minMessages: external_exports.number().min(3).default(5),
25657
+ maxSegment: external_exports.number().min(5).default(30),
25658
+ metaThreshold: external_exports.number().min(3).default(10),
25659
+ /** Max chars per tool output when rendering temporal messages for distillation input.
25660
+ * Outputs longer than this are replaced with a compact annotation preserving line
25661
+ * count, error signals, and file paths. Default: 2000 (matches upstream OpenCode's
25662
+ * TOOL_OUTPUT_MAX_CHARS during compaction). Set to 0 to disable. */
25663
+ toolOutputMaxChars: external_exports.number().min(0).default(2e3)
25664
+ }).default({
25665
+ minMessages: 5,
25666
+ maxSegment: 30,
25667
+ metaThreshold: 10,
25668
+ toolOutputMaxChars: 2e3
25669
+ }),
25498
25670
  knowledge: external_exports.object({
25499
25671
  /** Set to false to disable long-term knowledge storage and system-prompt injection.
25500
25672
  * Conversation recall (temporal search, distillation search) and context management
@@ -25599,6 +25771,7 @@ __export(embedding_exports, {
25599
25771
  vectorSearch: () => vectorSearch,
25600
25772
  vectorSearchDistillations: () => vectorSearchDistillations
25601
25773
  });
25774
+ var EMBED_TIMEOUT_MS = 1e4;
25602
25775
  var VOYAGE_API_URL = "https://api.voyageai.com/v1/embeddings";
25603
25776
  var VoyageProvider = class {
25604
25777
  maxBatchSize = 128;
@@ -25622,7 +25795,8 @@ var VoyageProvider = class {
25622
25795
  model: this.model,
25623
25796
  input_type: inputType,
25624
25797
  output_dimension: this.dimensions
25625
- })
25798
+ }),
25799
+ signal: AbortSignal.timeout(EMBED_TIMEOUT_MS)
25626
25800
  });
25627
25801
  if (!res.ok) {
25628
25802
  const body = await res.text().catch(() => "");
@@ -25658,7 +25832,8 @@ var OpenAIProvider = class {
25658
25832
  "Content-Type": "application/json",
25659
25833
  Authorization: `Bearer ${this.apiKey}`
25660
25834
  },
25661
- body: JSON.stringify(body)
25835
+ body: JSON.stringify(body),
25836
+ signal: AbortSignal.timeout(EMBED_TIMEOUT_MS)
25662
25837
  });
25663
25838
  if (!res.ok) {
25664
25839
  const responseBody = await res.text().catch(() => "");
@@ -26041,8 +26216,8 @@ function searchScored2(input) {
26041
26216
  const ftsSQL = `SELECT s.id, s.project_id, s.file, s.heading, s.depth, s.content,
26042
26217
  s.content_hash, s.first_paragraph, s.updated_at,
26043
26218
  bm25(lat_sections_fts, 6.0, 2.0) as rank
26044
- FROM lat_sections s
26045
- JOIN lat_sections_fts f ON s.rowid = f.rowid
26219
+ FROM lat_sections_fts f
26220
+ CROSS JOIN lat_sections s ON s.rowid = f.rowid
26046
26221
  WHERE lat_sections_fts MATCH ?
26047
26222
  AND s.project_id = ?
26048
26223
  ORDER BY rank LIMIT ?`;
@@ -26068,8 +26243,8 @@ function scoreForSession(projectPath, sessionContext, maxTokens) {
26068
26243
  `SELECT s.id, s.project_id, s.file, s.heading, s.depth, s.content,
26069
26244
  s.content_hash, s.first_paragraph, s.updated_at,
26070
26245
  bm25(lat_sections_fts, 6.0, 2.0) as rank
26071
- FROM lat_sections s
26072
- JOIN lat_sections_fts f ON s.rowid = f.rowid
26246
+ FROM lat_sections_fts f
26247
+ CROSS JOIN lat_sections s ON s.rowid = f.rowid
26073
26248
  WHERE lat_sections_fts MATCH ?
26074
26249
  AND s.project_id = ?
26075
26250
  ORDER BY rank`
@@ -26198,10 +26373,10 @@ function scoreEntriesFTS(sessionContext) {
26198
26373
  try {
26199
26374
  const results = db().query(
26200
26375
  `SELECT k.id, bm25(knowledge_fts, ?, ?, ?) as rank
26201
- FROM knowledge k
26202
- JOIN knowledge_fts f ON k.rowid = f.rowid
26203
- WHERE knowledge_fts MATCH ?
26204
- AND k.confidence > 0.2`
26376
+ FROM knowledge_fts f
26377
+ CROSS JOIN knowledge k ON k.rowid = f.rowid
26378
+ WHERE knowledge_fts MATCH ?
26379
+ AND k.confidence > 0.2`
26205
26380
  ).all(title, content3, category, q);
26206
26381
  if (!results.length) return /* @__PURE__ */ new Map();
26207
26382
  const ranks = results.map((r) => r.rank);
@@ -26335,13 +26510,13 @@ function search3(input) {
26335
26510
  const q = ftsQuery(input.query);
26336
26511
  if (q === EMPTY_QUERY) return [];
26337
26512
  const pid = input.projectPath ? ensureProject(input.projectPath) : null;
26338
- const ftsSQL = pid ? `SELECT ${KNOWLEDGE_COLS_K} FROM knowledge k
26339
- JOIN knowledge_fts f ON k.rowid = f.rowid
26513
+ const ftsSQL = pid ? `SELECT ${KNOWLEDGE_COLS_K} FROM knowledge_fts f
26514
+ CROSS JOIN knowledge k ON k.rowid = f.rowid
26340
26515
  WHERE knowledge_fts MATCH ?
26341
26516
  AND (k.project_id = ? OR k.project_id IS NULL OR k.cross_project = 1)
26342
26517
  AND k.confidence > 0.2
26343
- ORDER BY bm25(knowledge_fts, ?, ?, ?) LIMIT ?` : `SELECT ${KNOWLEDGE_COLS_K} FROM knowledge k
26344
- JOIN knowledge_fts f ON k.rowid = f.rowid
26518
+ ORDER BY bm25(knowledge_fts, ?, ?, ?) LIMIT ?` : `SELECT ${KNOWLEDGE_COLS_K} FROM knowledge_fts f
26519
+ CROSS JOIN knowledge k ON k.rowid = f.rowid
26345
26520
  WHERE knowledge_fts MATCH ?
26346
26521
  AND k.confidence > 0.2
26347
26522
  ORDER BY bm25(knowledge_fts, ?, ?, ?) LIMIT ?`;
@@ -26368,13 +26543,13 @@ function searchScored3(input) {
26368
26543
  if (q === EMPTY_QUERY) return [];
26369
26544
  const pid = input.projectPath ? ensureProject(input.projectPath) : null;
26370
26545
  const { title, content: content3, category } = ftsWeights();
26371
- const ftsSQL = pid ? `SELECT ${KNOWLEDGE_COLS_K}, bm25(knowledge_fts, ?, ?, ?) as rank FROM knowledge k
26372
- JOIN knowledge_fts f ON k.rowid = f.rowid
26546
+ const ftsSQL = pid ? `SELECT ${KNOWLEDGE_COLS_K}, bm25(knowledge_fts, ?, ?, ?) as rank FROM knowledge_fts f
26547
+ CROSS JOIN knowledge k ON k.rowid = f.rowid
26373
26548
  WHERE knowledge_fts MATCH ?
26374
26549
  AND (k.project_id = ? OR k.project_id IS NULL OR k.cross_project = 1)
26375
26550
  AND k.confidence > 0.2
26376
- ORDER BY rank LIMIT ?` : `SELECT ${KNOWLEDGE_COLS_K}, bm25(knowledge_fts, ?, ?, ?) as rank FROM knowledge k
26377
- JOIN knowledge_fts f ON k.rowid = f.rowid
26551
+ ORDER BY rank LIMIT ?` : `SELECT ${KNOWLEDGE_COLS_K}, bm25(knowledge_fts, ?, ?, ?) as rank FROM knowledge_fts f
26552
+ CROSS JOIN knowledge k ON k.rowid = f.rowid
26378
26553
  WHERE knowledge_fts MATCH ?
26379
26554
  AND k.confidence > 0.2
26380
26555
  ORDER BY rank LIMIT ?`;
@@ -26396,8 +26571,8 @@ function searchScoredOtherProjects(input) {
26396
26571
  if (q === EMPTY_QUERY) return [];
26397
26572
  const excludePid = ensureProject(input.excludeProjectPath);
26398
26573
  const { title, content: content3, category } = ftsWeights();
26399
- const ftsSQL = `SELECT ${KNOWLEDGE_COLS_K}, bm25(knowledge_fts, ?, ?, ?) as rank FROM knowledge k
26400
- JOIN knowledge_fts f ON k.rowid = f.rowid
26574
+ const ftsSQL = `SELECT ${KNOWLEDGE_COLS_K}, bm25(knowledge_fts, ?, ?, ?) as rank FROM knowledge_fts f
26575
+ CROSS JOIN knowledge k ON k.rowid = f.rowid
26401
26576
  WHERE knowledge_fts MATCH ?
26402
26577
  AND k.project_id IS NOT NULL
26403
26578
  AND k.project_id != ?
@@ -26530,8 +26705,8 @@ function check2(projectPath) {
26530
26705
  try {
26531
26706
  const { title, content: content3, category } = config2().search.ftsWeights;
26532
26707
  const matches = db().query(
26533
- `SELECT k.id, k.title FROM knowledge k
26534
- JOIN knowledge_fts f ON k.rowid = f.rowid
26708
+ `SELECT k.id, k.title FROM knowledge_fts f
26709
+ CROSS JOIN knowledge k ON k.rowid = f.rowid
26535
26710
  WHERE knowledge_fts MATCH ?
26536
26711
  AND k.id != ?
26537
26712
  AND k.confidence > 0.2
@@ -26565,9 +26740,13 @@ function check2(projectPath) {
26565
26740
  // src/distillation.ts
26566
26741
  var distillation_exports = {};
26567
26742
  __export(distillation_exports, {
26743
+ latestMetaObservations: () => latestMetaObservations,
26568
26744
  loadForSession: () => loadForSession,
26745
+ messagesToText: () => messagesToText,
26746
+ metaDistill: () => metaDistill,
26569
26747
  parseSourceIds: () => parseSourceIds,
26570
26748
  run: () => run,
26749
+ truncateToolOutputsInContent: () => truncateToolOutputsInContent,
26571
26750
  workerSessionIDs: () => workerSessionIDs
26572
26751
  });
26573
26752
 
@@ -26592,6 +26771,8 @@ function estimateMessage(msg) {
26592
26771
  }
26593
26772
  var contextLimit = 2e5;
26594
26773
  var outputReserved = 32e3;
26774
+ var maxLayer0Tokens = 0;
26775
+ var MIN_LAYER0_FLOOR = 4e4;
26595
26776
  var FIRST_TURN_OVERHEAD = 15e3;
26596
26777
  var calibratedOverhead = null;
26597
26778
  function makeSessionState() {
@@ -26605,7 +26786,11 @@ function makeSessionState() {
26605
26786
  forceMinLayer: 0,
26606
26787
  lastTransformEstimate: 0,
26607
26788
  prefixCache: null,
26608
- rawWindowCache: null
26789
+ rawWindowCache: null,
26790
+ lastTurnAt: 0,
26791
+ cameOutOfIdle: false,
26792
+ consecutiveHighLayer: 0,
26793
+ lastPrefixHash: ""
26609
26794
  };
26610
26795
  }
26611
26796
  var sessionStates = /* @__PURE__ */ new Map();
@@ -26618,11 +26803,36 @@ function getSessionState(sessionID) {
26618
26803
  }
26619
26804
  return state;
26620
26805
  }
26806
+ function onIdleResume(sessionID, thresholdMs, now = Date.now()) {
26807
+ if (thresholdMs <= 0) return { triggered: false };
26808
+ const state = getSessionState(sessionID);
26809
+ if (state.lastTurnAt === 0) return { triggered: false };
26810
+ const idleMs = now - state.lastTurnAt;
26811
+ if (idleMs < thresholdMs) return { triggered: false };
26812
+ state.prefixCache = null;
26813
+ state.rawWindowCache = null;
26814
+ state.cameOutOfIdle = true;
26815
+ return { triggered: true, idleMs };
26816
+ }
26817
+ function consumeCameOutOfIdle(sessionID) {
26818
+ const state = sessionStates.get(sessionID);
26819
+ if (!state || !state.cameOutOfIdle) return false;
26820
+ state.cameOutOfIdle = false;
26821
+ return true;
26822
+ }
26621
26823
  var ltmTokens = 0;
26622
26824
  function setModelLimits(limits) {
26623
26825
  contextLimit = limits.context || 2e5;
26624
26826
  outputReserved = Math.min(limits.output || 32e3, 32e3);
26625
26827
  }
26828
+ function setMaxLayer0Tokens(tokens) {
26829
+ maxLayer0Tokens = Math.max(0, Math.floor(tokens));
26830
+ }
26831
+ function computeLayer0Cap(targetCostPerTurn, cacheReadCostPerToken) {
26832
+ if (targetCostPerTurn <= 0 || cacheReadCostPerToken <= 0) return 0;
26833
+ const rawCap = Math.floor(targetCostPerTurn / cacheReadCostPerToken);
26834
+ return Math.max(rawCap, MIN_LAYER0_FLOOR);
26835
+ }
26626
26836
  function setLtmTokens(tokens) {
26627
26837
  ltmTokens = tokens;
26628
26838
  }
@@ -26667,6 +26877,19 @@ function setForceMinLayer(layer, sessionID) {
26667
26877
  }
26668
26878
  }
26669
26879
  }
26880
+ function inspectSessionState(sessionID) {
26881
+ const state = sessionStates.get(sessionID);
26882
+ if (!state) return null;
26883
+ return {
26884
+ hasPrefixCache: state.prefixCache !== null,
26885
+ hasRawWindowCache: state.rawWindowCache !== null,
26886
+ cameOutOfIdle: state.cameOutOfIdle,
26887
+ lastTurnAt: state.lastTurnAt
26888
+ };
26889
+ }
26890
+ function setLastTurnAtForTest(sessionID, ms) {
26891
+ getSessionState(sessionID).lastTurnAt = ms;
26892
+ }
26670
26893
  function loadDistillations(projectPath, sessionID) {
26671
26894
  const pid = ensureProject(projectPath);
26672
26895
  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";
@@ -26699,12 +26922,17 @@ function cleanParts(parts) {
26699
26922
  }
26700
26923
  return filtered.length > 0 ? filtered : parts;
26701
26924
  }
26925
+ var ANNOTATION_PATH_SCAN_LIMIT = 64 * 1024;
26926
+ var PATH_RE = /(?:[\w.-]+\/)+[\w.-]+\.\w{1,5}/g;
26702
26927
  function toolStripAnnotation(toolName, output) {
26703
26928
  const lines = output.split("\n").length;
26704
- const chars = output.length;
26705
26929
  const hasError = /\b(?:error|fail(?:ed|ure)?|exception|panic|traceback)\b/i.test(output);
26706
- const paths = output.match(/(?:[\w.-]+\/)+[\w.-]+\.\w{1,5}/g);
26707
- const uniquePaths = paths ? [...new Set(paths)].slice(0, 5) : [];
26930
+ let uniquePaths = [];
26931
+ if (output.indexOf("/") !== -1) {
26932
+ const pathScan = output.length > ANNOTATION_PATH_SCAN_LIMIT ? output.slice(0, ANNOTATION_PATH_SCAN_LIMIT) : output;
26933
+ const paths = pathScan.match(PATH_RE);
26934
+ if (paths) uniquePaths = [...new Set(paths)].slice(0, 5);
26935
+ }
26708
26936
  let annotation = `[output omitted \u2014 ${toolName}: ${lines} lines`;
26709
26937
  if (hasError) annotation += ", contained errors";
26710
26938
  if (uniquePaths.length > 0) annotation += `, paths: ${uniquePaths.join(", ")}`;
@@ -27135,7 +27363,11 @@ function transformInner(input) {
27135
27363
  expectedInput = messageTokens + overhead + ltmTokens;
27136
27364
  }
27137
27365
  const layer0Input = calibrated ? expectedInput : expectedInput * UNCALIBRATED_SAFETY;
27138
- if (effectiveMinLayer === 0 && layer0Input <= maxInput) {
27366
+ let layer0Ceiling = maxLayer0Tokens > 0 ? Math.min(maxInput, maxLayer0Tokens) : maxInput;
27367
+ if (!calibrated && layer0Ceiling < maxInput) {
27368
+ layer0Ceiling = Math.floor(layer0Ceiling * 0.7);
27369
+ }
27370
+ if (effectiveMinLayer === 0 && layer0Input <= layer0Ceiling) {
27139
27371
  const messageTokens = calibrated ? expectedInput - (ltmTokens - sessState.lastKnownLtm) : expectedInput - overhead - ltmTokens;
27140
27372
  return {
27141
27373
  messages: input.messages,
@@ -27216,14 +27448,31 @@ function transformInner(input) {
27216
27448
  (sum, m) => sum + estimateMessage(m),
27217
27449
  0
27218
27450
  );
27219
- const nuclearRaw = input.messages.slice(-3).map((m) => ({
27451
+ const tailBudget = Math.max(2e3, Math.min(8e3, Math.floor(usable * 0.25)));
27452
+ const nuclearTurnStart = currentTurnStart(input.messages);
27453
+ const currentTurn = input.messages.slice(nuclearTurnStart).map((m) => ({
27220
27454
  info: m.info,
27221
27455
  parts: cleanParts(m.parts)
27222
27456
  }));
27223
- const nuclearRawTokens = nuclearRaw.reduce(
27457
+ const currentTurnTokens = currentTurn.reduce(
27224
27458
  (sum, m) => sum + estimateMessage(m),
27225
27459
  0
27226
27460
  );
27461
+ const olderMessages = [];
27462
+ let olderTokens = 0;
27463
+ const remaining = Math.max(0, tailBudget - currentTurnTokens);
27464
+ for (let i = nuclearTurnStart - 1; i >= 0 && olderTokens < remaining; i--) {
27465
+ const msg = input.messages[i];
27466
+ const est = estimateMessage(msg);
27467
+ if (olderTokens + est > remaining) break;
27468
+ olderMessages.unshift({
27469
+ info: msg.info,
27470
+ parts: cleanParts(msg.parts)
27471
+ });
27472
+ olderTokens += est;
27473
+ }
27474
+ const nuclearRaw = [...olderMessages, ...currentTurn];
27475
+ const nuclearRawTokens = olderTokens + currentTurnTokens;
27227
27476
  return {
27228
27477
  messages: [...nuclearPrefix, ...nuclearRaw],
27229
27478
  layer: 4,
@@ -27245,19 +27494,55 @@ function transform2(input) {
27245
27494
  state.lastTransformEstimate = result.totalTokens;
27246
27495
  state.lastLayer = result.layer;
27247
27496
  state.lastWindowMessageIDs = new Set(result.messages.map((m) => m.info.id));
27497
+ state.lastTurnAt = Date.now();
27498
+ const prefixIds = result.messages.slice(0, 5).map((m) => m.info.id).join(",");
27499
+ const prefixHash = `${result.layer}:${prefixIds}`;
27500
+ if (state.lastPrefixHash && state.lastPrefixHash !== prefixHash) {
27501
+ info(
27502
+ `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)}`
27503
+ );
27504
+ }
27505
+ state.lastPrefixHash = prefixHash;
27506
+ if (result.layer >= 2) {
27507
+ state.consecutiveHighLayer++;
27508
+ if (state.consecutiveHighLayer === 3) {
27509
+ info(
27510
+ `session ${sid} has been at gradient layer ${result.layer}+ for 3 consecutive turns. Consider running /compact to reset the context window.`
27511
+ );
27512
+ }
27513
+ } else {
27514
+ state.consecutiveHighLayer = 0;
27515
+ }
27516
+ info(
27517
+ `gradient: session=${sid} layer=${result.layer} tokens=${result.totalTokens} (distilled=${result.distilledTokens} raw=${result.rawTokens}) usable=${result.usable} cap=${maxLayer0Tokens || "off"}`
27518
+ );
27248
27519
  }
27249
27520
  return result;
27250
27521
  }
27251
27522
  function currentTurnStart(messages) {
27252
- let lastUserIdx = -1;
27523
+ if (messages.length === 0) return 0;
27524
+ let boundary = messages.length;
27253
27525
  for (let i = messages.length - 1; i >= 0; i--) {
27254
27526
  if (messages[i].info.role === "user") {
27255
- lastUserIdx = i;
27527
+ boundary = i;
27256
27528
  break;
27257
27529
  }
27258
27530
  }
27259
- if (lastUserIdx === -1) return 0;
27260
- return lastUserIdx;
27531
+ if (boundary === messages.length) return 0;
27532
+ for (let i = boundary - 1; i >= 0; i--) {
27533
+ const msg = messages[i];
27534
+ const hasToolParts = msg.parts.some(isToolPart);
27535
+ if (hasToolParts) {
27536
+ boundary = i;
27537
+ continue;
27538
+ }
27539
+ if (msg.info.role === "user") {
27540
+ boundary = i;
27541
+ continue;
27542
+ }
27543
+ break;
27544
+ }
27545
+ return boundary;
27261
27546
  }
27262
27547
  function tryFit(input) {
27263
27548
  if (input.prefixTokens > input.distilledBudget && input.prefix.length > 0)
@@ -27338,8 +27623,39 @@ function formatTime(ms) {
27338
27623
  const m = d.getMinutes().toString().padStart(2, "0");
27339
27624
  return `${h3}:${m}`;
27340
27625
  }
27341
- function messagesToText(messages) {
27342
- return messages.map((m) => `[${m.role}] (${formatTime(m.created_at)}) ${m.content}`).join("\n\n");
27626
+ var CHUNK_SEPARATOR = "\n" + CHUNK_TERMINATOR;
27627
+ function truncateToolOutputsInContent(content3, maxChars) {
27628
+ if (maxChars <= 0 || content3.length === 0) return content3;
27629
+ if (content3.indexOf(CHUNK_TERMINATOR) === -1) {
27630
+ return truncateSingleChunk(content3, maxChars);
27631
+ }
27632
+ const chunks = content3.split(CHUNK_SEPARATOR);
27633
+ let anyToolChunk = false;
27634
+ for (const c of chunks) {
27635
+ if (c.startsWith("[tool:")) {
27636
+ anyToolChunk = true;
27637
+ break;
27638
+ }
27639
+ }
27640
+ if (!anyToolChunk) return content3;
27641
+ const out = chunks.map((chunk) => truncateSingleChunk(chunk, maxChars));
27642
+ return out.join(CHUNK_SEPARATOR);
27643
+ }
27644
+ function truncateSingleChunk(chunk, maxChars) {
27645
+ if (!chunk.startsWith("[tool:")) return chunk;
27646
+ const closeBracket = chunk.indexOf("] ");
27647
+ if (closeBracket < 0) return chunk;
27648
+ const toolName = chunk.slice(6, closeBracket);
27649
+ const payload = chunk.slice(closeBracket + 2);
27650
+ if (payload.length <= maxChars) return chunk;
27651
+ return `[tool:${toolName}] ${toolStripAnnotation(toolName, payload)}`;
27652
+ }
27653
+ function messagesToText(messages, toolOutputMaxChars) {
27654
+ const cap = toolOutputMaxChars ?? config2().distillation.toolOutputMaxChars;
27655
+ return messages.map((m) => {
27656
+ const body = m.role === "user" ? m.content : truncateToolOutputsInContent(m.content, cap);
27657
+ return `[${m.role}] (${formatTime(m.created_at)}) ${body}`;
27658
+ }).join("\n\n");
27343
27659
  }
27344
27660
  function parseDistillationResult(text4) {
27345
27661
  const match = text4.match(/<observations>([\s\S]*?)<\/observations>/i);
@@ -27354,6 +27670,19 @@ function latestObservations(projectPath, sessionID) {
27354
27670
  ).get(pid, sessionID);
27355
27671
  return row?.observations || void 0;
27356
27672
  }
27673
+ function latestMetaObservations(projectPath, sessionID) {
27674
+ return latestMeta(projectPath, sessionID)?.observations;
27675
+ }
27676
+ function latestMeta(projectPath, sessionID) {
27677
+ const pid = ensureProject(projectPath);
27678
+ const row = db().query(
27679
+ `SELECT observations, generation FROM distillations
27680
+ WHERE project_id = ? AND session_id = ? AND generation > 0
27681
+ ORDER BY generation DESC, created_at DESC LIMIT 1`
27682
+ ).get(pid, sessionID);
27683
+ if (!row || !row.observations) return void 0;
27684
+ return row;
27685
+ }
27357
27686
  function parseSourceIds(raw) {
27358
27687
  try {
27359
27688
  const parsed = JSON.parse(raw);
@@ -27363,11 +27692,10 @@ function parseSourceIds(raw) {
27363
27692
  return [];
27364
27693
  }
27365
27694
  }
27366
- function loadForSession(projectPath, sessionID) {
27695
+ function loadForSession(projectPath, sessionID, includeArchived = false) {
27367
27696
  const pid = ensureProject(projectPath);
27368
- const rows = db().query(
27369
- "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"
27370
- ).all(pid, sessionID);
27697
+ 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";
27698
+ const rows = db().query(sql).all(pid, sessionID);
27371
27699
  return rows.map((r) => ({
27372
27700
  ...r,
27373
27701
  source_ids: parseSourceIds(r.source_ids)
@@ -27532,8 +27860,13 @@ async function distillSegment(input) {
27532
27860
  }
27533
27861
  async function metaDistill(input) {
27534
27862
  const existing = loadGen0(input.projectPath, input.sessionID);
27535
- if (existing.length < 3) return null;
27536
- const userContent = recursiveUser(existing);
27863
+ const priorMeta = latestMeta(input.projectPath, input.sessionID);
27864
+ if (priorMeta) {
27865
+ if (existing.length === 0) return null;
27866
+ } else {
27867
+ if (existing.length < 3) return null;
27868
+ }
27869
+ const userContent = recursiveUser(existing, priorMeta?.observations);
27537
27870
  const model = input.model ?? config2().model;
27538
27871
  const responseText = await input.llm.prompt(
27539
27872
  RECURSIVE_SYSTEM,
@@ -27543,19 +27876,30 @@ async function metaDistill(input) {
27543
27876
  if (!responseText) return null;
27544
27877
  const result = parseDistillationResult(responseText);
27545
27878
  if (!result) return null;
27546
- const maxGen = Math.max(...existing.map((d) => d.generation));
27879
+ const maxGen = Math.max(
27880
+ ...existing.map((d) => d.generation),
27881
+ priorMeta?.generation ?? 0
27882
+ );
27547
27883
  const allSourceIDs = existing.flatMap((d) => d.source_ids);
27548
- const metaId = storeDistillation({
27549
- projectPath: input.projectPath,
27550
- sessionID: input.sessionID,
27551
- observations: result.observations,
27552
- sourceIDs: allSourceIDs,
27553
- generation: maxGen + 1
27554
- });
27884
+ let metaId;
27885
+ db().exec("BEGIN IMMEDIATE");
27886
+ try {
27887
+ metaId = storeDistillation({
27888
+ projectPath: input.projectPath,
27889
+ sessionID: input.sessionID,
27890
+ observations: result.observations,
27891
+ sourceIDs: allSourceIDs,
27892
+ generation: maxGen + 1
27893
+ });
27894
+ archiveDistillations(existing.map((d) => d.id));
27895
+ db().exec("COMMIT");
27896
+ } catch (e) {
27897
+ db().exec("ROLLBACK");
27898
+ throw e;
27899
+ }
27555
27900
  if (isAvailable()) {
27556
27901
  embedDistillation(metaId, result.observations);
27557
27902
  }
27558
- archiveDistillations(existing.map((d) => d.id));
27559
27903
  return result;
27560
27904
  }
27561
27905
 
@@ -27714,13 +28058,13 @@ function searchDistillationsScored(input) {
27714
28058
  const q = ftsQuery(input.query);
27715
28059
  if (q === EMPTY_QUERY) return [];
27716
28060
  const ftsSQL = input.sessionID ? `SELECT d.id, d.observations, d.generation, d.created_at, d.session_id, rank
27717
- FROM distillations d
27718
- JOIN distillation_fts f ON d.rowid = f.rowid
28061
+ FROM distillation_fts f
28062
+ CROSS JOIN distillations d ON d.rowid = f.rowid
27719
28063
  WHERE distillation_fts MATCH ?
27720
28064
  AND d.project_id = ? AND d.session_id = ?
27721
28065
  ORDER BY rank LIMIT ?` : `SELECT d.id, d.observations, d.generation, d.created_at, d.session_id, rank
27722
- FROM distillations d
27723
- JOIN distillation_fts f ON d.rowid = f.rowid
28066
+ FROM distillation_fts f
28067
+ CROSS JOIN distillations d ON d.rowid = f.rowid
27724
28068
  WHERE distillation_fts MATCH ?
27725
28069
  AND d.project_id = ?
27726
28070
  ORDER BY rank LIMIT ?`;
@@ -28163,7 +28507,130 @@ function importFromFile(input) {
28163
28507
  }
28164
28508
  }
28165
28509
  }
28510
+
28511
+ // src/worker-model.ts
28512
+ var worker_model_exports = {};
28513
+ __export(worker_model_exports, {
28514
+ WORKER_JUDGE_SYSTEM: () => WORKER_JUDGE_SYSTEM,
28515
+ computeModelFingerprint: () => computeModelFingerprint,
28516
+ getValidatedWorkerModel: () => getValidatedWorkerModel,
28517
+ isValidationStale: () => isValidationStale,
28518
+ parseJudgeScore: () => parseJudgeScore,
28519
+ resolveWorkerModel: () => resolveWorkerModel,
28520
+ selectWorkerCandidates: () => selectWorkerCandidates,
28521
+ storeValidatedWorkerModel: () => storeValidatedWorkerModel,
28522
+ structuralCheck: () => structuralCheck,
28523
+ workerJudgeUser: () => workerJudgeUser
28524
+ });
28525
+ var KV_PREFIX = "lore:worker_model:";
28526
+ function selectWorkerCandidates(sessionModel, providerModels) {
28527
+ const eligible = providerModels.filter(
28528
+ (m) => m.providerID === sessionModel.providerID && m.status === "active" && m.capabilities.input.text
28529
+ );
28530
+ if (eligible.length === 0) return [];
28531
+ const sorted = [...eligible].sort((a, b) => a.cost.input - b.cost.input);
28532
+ const cheapest = sorted[0];
28533
+ const belowSession = sorted.filter((m) => m.cost.input < sessionModel.cost.input).pop();
28534
+ const candidates = /* @__PURE__ */ new Map();
28535
+ candidates.set(cheapest.id, cheapest);
28536
+ if (belowSession && belowSession.id !== cheapest.id) {
28537
+ candidates.set(belowSession.id, belowSession);
28538
+ }
28539
+ if (cheapest.id === sessionModel.id || cheapest.cost.input >= sessionModel.cost.input) {
28540
+ return [cheapest];
28541
+ }
28542
+ return [...candidates.values()];
28543
+ }
28544
+ function computeModelFingerprint(providerID, sessionModelID, activeModelIDs) {
28545
+ const sorted = [...activeModelIDs].sort();
28546
+ return sha256(
28547
+ JSON.stringify({ providerID, sessionModelID, modelIDs: sorted })
28548
+ );
28549
+ }
28550
+ function getValidatedWorkerModel(providerID) {
28551
+ const row = db().query("SELECT value FROM kv_meta WHERE key = ?").get(`${KV_PREFIX}${providerID}`);
28552
+ if (!row) return null;
28553
+ try {
28554
+ return JSON.parse(row.value);
28555
+ } catch {
28556
+ return null;
28557
+ }
28558
+ }
28559
+ function storeValidatedWorkerModel(result) {
28560
+ const key = `${KV_PREFIX}${result.providerID}`;
28561
+ const value = JSON.stringify(result);
28562
+ db().query(
28563
+ "INSERT INTO kv_meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = ?"
28564
+ ).run(key, value, value);
28565
+ }
28566
+ function isValidationStale(stored, currentFingerprint) {
28567
+ if (!stored) return true;
28568
+ return stored.fingerprint !== currentFingerprint;
28569
+ }
28570
+ function structuralCheck(candidateObservations, referenceObservations) {
28571
+ if (candidateObservations == null || candidateObservations.length === 0) {
28572
+ return { passed: false, observationCount: 0, tokenCount: 0, reason: candidateObservations === null ? "parse_failed" : "empty" };
28573
+ }
28574
+ const countObs = (text4) => text4.split("\n").filter((l) => l.trim().length > 0).length;
28575
+ const refCount = countObs(referenceObservations);
28576
+ const candCount = countObs(candidateObservations);
28577
+ const candTokens = Math.ceil(candidateObservations.length / 3);
28578
+ if (refCount > 0 && (candCount < refCount * 0.5 || candCount > refCount * 1.5)) {
28579
+ return {
28580
+ passed: false,
28581
+ observationCount: candCount,
28582
+ tokenCount: candTokens,
28583
+ reason: `observation_count_${candCount}_vs_ref_${refCount}`
28584
+ };
28585
+ }
28586
+ const refTokens = Math.ceil(referenceObservations.length / 3);
28587
+ if (candTokens === 0) {
28588
+ return { passed: false, observationCount: candCount, tokenCount: candTokens, reason: "empty" };
28589
+ }
28590
+ if (refTokens > 0 && candTokens > refTokens * 3) {
28591
+ return {
28592
+ passed: false,
28593
+ observationCount: candCount,
28594
+ tokenCount: candTokens,
28595
+ reason: `token_count_${candTokens}_vs_ref_${refTokens}_3x`
28596
+ };
28597
+ }
28598
+ return { passed: true, observationCount: candCount, tokenCount: candTokens };
28599
+ }
28600
+ 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.
28601
+
28602
+ Rate the candidate on a scale of 1-5:
28603
+ 5 = Captures all key facts and decisions, equivalent to reference
28604
+ 4 = Captures most facts, minor omissions
28605
+ 3 = Captures the essential facts, some detail loss acceptable
28606
+ 2 = Missing important facts or technical details
28607
+ 1 = Significantly incomplete or inaccurate
28608
+
28609
+ Respond with ONLY a single digit (1-5).`;
28610
+ function workerJudgeUser(reference, candidate) {
28611
+ return `<reference>
28612
+ ${reference}
28613
+ </reference>
28614
+
28615
+ <candidate>
28616
+ ${candidate}
28617
+ </candidate>`;
28618
+ }
28619
+ function parseJudgeScore(response) {
28620
+ const match = response.trim().match(/^([1-5])/);
28621
+ if (!match) return null;
28622
+ return parseInt(match[1], 10);
28623
+ }
28624
+ function resolveWorkerModel(providerID, configWorkerModel, configModel) {
28625
+ if (configWorkerModel) return configWorkerModel;
28626
+ const validated = getValidatedWorkerModel(providerID);
28627
+ if (validated) {
28628
+ return { providerID: validated.providerID, modelID: validated.modelID };
28629
+ }
28630
+ return configModel;
28631
+ }
28166
28632
  export {
28633
+ COMPACT_SUMMARY_TEMPLATE,
28167
28634
  CONSOLIDATION_SYSTEM,
28168
28635
  CURATOR_SYSTEM,
28169
28636
  DISTILLATION_SYSTEM,
@@ -28172,10 +28639,14 @@ export {
28172
28639
  RECALL_PARAM_DESCRIPTIONS,
28173
28640
  RECALL_TOOL_DESCRIPTION,
28174
28641
  RECURSIVE_SYSTEM,
28642
+ WORKER_JUDGE_SYSTEM,
28643
+ buildCompactPrompt,
28175
28644
  calibrate,
28176
28645
  close,
28646
+ computeLayer0Cap,
28177
28647
  config2 as config,
28178
28648
  consolidationUser,
28649
+ consumeCameOutOfIdle,
28179
28650
  curator_exports as curator,
28180
28651
  curatorUser,
28181
28652
  db,
@@ -28197,6 +28668,7 @@ export {
28197
28668
  h,
28198
28669
  importFromFile,
28199
28670
  inline,
28671
+ inspectSessionState,
28200
28672
  isFirstRun,
28201
28673
  isReasoningPart,
28202
28674
  isTextPart,
@@ -28211,6 +28683,7 @@ export {
28211
28683
  ltm_exports as ltm,
28212
28684
  needsUrgentDistillation,
28213
28685
  normalize,
28686
+ onIdleResume,
28214
28687
  p,
28215
28688
  projectId,
28216
28689
  projectName,
@@ -28222,15 +28695,20 @@ export {
28222
28695
  saveForceMinLayer,
28223
28696
  serialize,
28224
28697
  setForceMinLayer,
28698
+ setLastTurnAtForTest,
28225
28699
  setLtmTokens,
28700
+ setMaxLayer0Tokens,
28226
28701
  setModelLimits,
28227
28702
  shouldImport,
28228
28703
  strong2 as strong,
28229
28704
  t,
28230
28705
  temporal_exports as temporal,
28706
+ toolStripAnnotation,
28231
28707
  transform2 as transform,
28232
28708
  ul,
28233
28709
  unescapeMarkdown,
28710
+ workerJudgeUser,
28711
+ worker_model_exports as workerModel,
28234
28712
  workerSessionIDs
28235
28713
  };
28236
28714
  //# sourceMappingURL=index.js.map