lossless-core 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # lossless-core
2
2
 
3
+ ## 0.1.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 470a7b7: Verify file bytes and sha256 against the linked message text before inserting file rows on import. A repeated entry id with different text, or a file row whose text differs from the stored message, also refuses the import without writing. Existing rows stay untouched.
8
+ - 470a7b7: Signal a watermark miss on the ingest path. A full rescan now writes one `ingest-rescan` metric row and, in interactive sessions, one UI warning. Empty entries stay silent and ingest behavior is unchanged.
9
+ - 470a7b7: Add a mechanical tier for noisy tools. Name tools in `mechanicalTools` in summarizer-config, `/lcm` config, or the OpenCode plugin options. Their spans compact into deterministic `mechanical` leaves with zero summarizer spend at compaction time. Every message stays stored verbatim and covered, so the integrity coverage check passes, and heal re-derives a skipped leaf from provenance on demand. There is no ingest filtering.
10
+ - 470a7b7: Report orphan sidecars in doctor. `integritySnapshot` now lists sidecar files and stale `wal` and `shm` files with no live session (`.sqlite` by default, `.db` for Pi). `checkIntegrity` reports them as a read-only `orphan-sidecar` finding through `lcm doctor` and the diagnose bundle. Report only. Deletion stays manual.
11
+ - 470a7b7: Upgrade recall search. `lcm_grep` takes `role`, `date_start`/`date_end`, and `tool_name` filters. Role and time are SQL predicates on the stored columns with a covering index. Tool scope is an FTS rewrite against `[tool:Name]` markers with no new table or index. `lcm_expand` takes an `anchor` entry id that centers the listing window with edge clamping. The recall manifest and the retrieval brief order by stop-word stripped query-word overlap over summary headers. A read-only files view lists each linked file once with an incomplete-coverage flag and a `large-inline` mark. No migration and no new writer.
12
+ - 0c70748: Ask providers for a whole number of tokens. A leaf target was half of the calibrated chunk budget, so it was fractional whenever that budget was odd, and a provider that validates `max_tokens` as an unsigned integer rejected the call with a 422. The ladder then stored the deterministic truncate: in one week of Pi sessions that was 1,315 of 5,358 summaries, with 540 rejected calls naming values such as `max_tokens: 5543.5`. The request builder now rounds to nearest, never below one, and treats a non-finite budget as that floor, before the reasoning headroom is added. Integer budgets produce the same request as before. No config key, migration or metric row changes.
13
+
3
14
  ## 0.1.2
4
15
 
5
16
  ### Patch Changes
package/dist/index.d.mts CHANGED
@@ -198,6 +198,15 @@ export interface IntegritySnapshot {
198
198
  foreignKeyViolations: number;
199
199
  schemaVersion: number;
200
200
  uncoveredMessages: number;
201
+ /** Full paths, sorted, so doctor can name what a manual drop would remove. */
202
+ orphanSidecars: string[];
203
+ }
204
+ export interface IntegritySnapshotOptions {
205
+ /** Paths with an open store in this process; every other sidecar file in
206
+ * `sidecarDir` reports as unopened, even when its session still exists. */
207
+ liveSidecarPaths?: readonly string[];
208
+ sidecarDir?: string;
209
+ sidecarExtension?: string;
201
210
  }
202
211
  /** A fresh row, or the row that already owns the span; `existingTokens` comes
203
212
  * from that row, so a caller reports the collision, not its intended write.
@@ -454,14 +463,23 @@ export declare class LcmStore {
454
463
  offset?: number;
455
464
  withinMessageIds?: ReadonlySet<number>;
456
465
  includeRemoved?: boolean;
466
+ role?: LcmRole;
467
+ dateStart?: number;
468
+ dateEnd?: number;
457
469
  }): GrepHit[];
458
470
  grepCount(pattern: string, opts?: {
459
471
  withinMessageIds?: ReadonlySet<number>;
460
472
  includeRemoved?: boolean;
473
+ role?: LcmRole;
474
+ dateStart?: number;
475
+ dateEnd?: number;
461
476
  }): number | undefined;
462
477
  messagePage(afterRowId: number, limit: number, opts?: {
463
478
  withinMessageIds?: ReadonlySet<number>;
464
479
  includeRemoved?: boolean;
480
+ role?: LcmRole;
481
+ dateStart?: number;
482
+ dateEnd?: number;
465
483
  }): ScannedRow[];
466
484
  coveringSummaryId(entryId: string): number | undefined;
467
485
  messageByEntryId(entryId: string): StoredMessage | undefined;
@@ -478,7 +496,7 @@ export declare class LcmStore {
478
496
  getSummary(id: number, run?: string): SummaryNode | undefined;
479
497
  /** Bounded by a visited set, so a corrupt cycle cannot recurse forever. */
480
498
  coveredMessageIds(summaryId: number, seen?: Set<number>): number[];
481
- integritySnapshot(): IntegritySnapshot;
499
+ integritySnapshot(opts?: IntegritySnapshotOptions): IntegritySnapshot;
482
500
  /** Restore the full-text index. The schema is re-applied first, because a
483
501
  * dropped virtual table cannot be rebuilt, only recreated, and the DDL that
484
502
  * knows how is the schema itself. Everything in it is `IF NOT EXISTS` apart
@@ -538,10 +556,12 @@ export declare const NO_STORE = "LCM: no stored history for this session yet (no
538
556
  export declare const RECALL_SCOPES: readonly ["session", "sessions", "all_sessions"];
539
557
  /** One of `RECALL_SCOPES`; `sessions` and `all_sessions` are equivalent and both read past sidecars. */
540
558
  export type RecallScope = (typeof RECALL_SCOPES)[number];
541
- /** What `lcm_expand` reads: a summary listing by id, or one message by entry id with an optional summary check. */
559
+ /** What `lcm_expand` reads: a summary listing by id (optionally centered on an
560
+ * anchor entry), or one message by entry id with an optional summary check. */
542
561
  export type Address = {
543
562
  readonly kind: "summary";
544
563
  readonly id: number;
564
+ readonly anchor?: string;
545
565
  } | {
546
566
  readonly kind: "entry";
547
567
  readonly entryId: string;
@@ -569,18 +589,27 @@ export interface RecallToolDeps {
569
589
  readonly record?: (row: Record<string, unknown>) => void;
570
590
  readonly sessionOf?: (sessionId: string) => string;
571
591
  readonly isReader?: (sessionId: string) => boolean;
572
- readonly searchPast?: (session: string, query: string, scope: Exclude<RecallScope, "session">, limit: unknown) => RecallPastSearchResult;
592
+ readonly searchPast?: (session: string, query: string, scope: Exclude<RecallScope, "session">, limit: unknown, filters?: GrepFilterArgs) => RecallPastSearchResult;
573
593
  readonly openPast?: (session: string, pastSession: string) => OpenPastResult;
574
594
  readonly pastSearchUnreachableText?: string;
575
595
  readonly pastReadUnreachableText?: string;
576
596
  }
577
597
  /** Answer `lcm_grep`: the session `grepView`, or past sidecars when the scope names them; readers stay session-scoped. */
578
598
  export declare function grepAnswer(deps: RecallToolDeps, callerSessionID: string, input: unknown): string;
599
+ /** Optional `lcm_grep` filters, shared by the session search and the past-session
600
+ * scan. Unknown roles and bad timestamps are caller errors; an empty tool name
601
+ * is too, while an unknown-but-well-formed tool name simply matches nothing. */
602
+ export interface GrepFilterArgs {
603
+ role?: "user" | "assistant" | "toolResult" | "custom";
604
+ dateStart?: number;
605
+ dateEnd?: number;
606
+ toolName?: string;
607
+ }
579
608
  /** Answer `lcm_describe`: the redacted cost and contents of one summary id, or why it missed. */
580
609
  export declare function describeAnswer(deps: RecallToolDeps, callerSessionID: string, input: unknown): string;
581
610
  /** Answer `lcm_expand`: list a summary or read one message, capped at `MAX_EXPAND_CHARS`; a foreign `session` id reads a past sidecar read-only. */
582
611
  export declare function expandAnswer(deps: RecallToolDeps, callerSessionID: string, input: unknown): string;
583
- /** Parse `id`/`entry_id` args into an `Address`: error text when malformed, undefined when neither is present. */
612
+ /** Parse `id`/`entry_id`/`anchor` args into an `Address`: error text when malformed, undefined when neither is present. */
584
613
  export declare function readAddress(args: Record<string, unknown>): Address | string | undefined;
585
614
  /** Refusal text when an address names no stored summary or message, else undefined. */
586
615
  export declare function addressProblem(store: LcmStore, address: Address): string | undefined;
@@ -1239,6 +1268,12 @@ export type BranchRemovedRow = LcmRowBase & {
1239
1268
  removed: number;
1240
1269
  restored: number;
1241
1270
  };
1271
+ /** A watermark miss forced a full rescan. Empty deltas never write one. */
1272
+ export type IngestRescanRow = LcmRowBase & {
1273
+ kind: "ingest-rescan";
1274
+ watermark: string;
1275
+ entries: number;
1276
+ };
1242
1277
  export type FileExternalizedRow = LcmRowBase & {
1243
1278
  kind: "file-externalized";
1244
1279
  entryId: string;
@@ -1310,7 +1345,7 @@ export type ImportRow = MetricBase & {
1310
1345
  summariesInserted: number;
1311
1346
  summariesSkipped: number;
1312
1347
  });
1313
- export type LcmMetricRecord = ContextDecisionRow | ProjectionOverBudgetRow | SwapAppliedRow | SwapBlockedRow | PiCompactionRow | EstimateCalibratedRow | WindowCorrectedRow | WindowFloorRow | ConfigProblemRow | SummaryInputClippedRow | SummaryFallbackStoredRow | SummaryStoredRow | SummarySkippedCoveredRow | SummarySkippedExistingRow | SummaryOversizedRow | SummaryReplacedRow | SummaryFinalTruncatedRow | CondensationStalledRow | CondensationReducedRow | CompactionCompleteRow | HealStartRow | HealNodeRow | HealCompleteRow | SummarizerErrorRow | SummarizerCappedRow | SummarizerFallbackRow | SummarizerModelRow | SummarizerUsageRow | CompactionErrorRow | CompactionAwaitedRow | CompactionQueuedRow | CompactionQueuedDroppedRow | CompactionTimeoutClearedRow | CompactionStaleRow | CompactionAbandonedRow | CompactionNoopRow | GcDeletedRow | StoreMigratedRow | BranchRemovedRow | FileExternalizedRow | LargeInlineRow | RecallRow | RetrievalRow | EvalRow | ImportRow;
1348
+ export type LcmMetricRecord = ContextDecisionRow | ProjectionOverBudgetRow | SwapAppliedRow | SwapBlockedRow | PiCompactionRow | EstimateCalibratedRow | WindowCorrectedRow | WindowFloorRow | ConfigProblemRow | SummaryInputClippedRow | SummaryFallbackStoredRow | SummaryStoredRow | SummarySkippedCoveredRow | SummarySkippedExistingRow | SummaryOversizedRow | SummaryReplacedRow | SummaryFinalTruncatedRow | CondensationStalledRow | CondensationReducedRow | CompactionCompleteRow | HealStartRow | HealNodeRow | HealCompleteRow | SummarizerErrorRow | SummarizerCappedRow | SummarizerFallbackRow | SummarizerModelRow | SummarizerUsageRow | CompactionErrorRow | CompactionAwaitedRow | CompactionQueuedRow | CompactionQueuedDroppedRow | CompactionTimeoutClearedRow | CompactionStaleRow | CompactionAbandonedRow | CompactionNoopRow | GcDeletedRow | StoreMigratedRow | BranchRemovedRow | IngestRescanRow | FileExternalizedRow | LargeInlineRow | RecallRow | RetrievalRow | EvalRow | ImportRow;
1314
1349
  export type MetricRecord = UsageRow | LcmMetricRecord;
1315
1350
  export interface LlmComplete {
1316
1351
  (systemPrompt: string, userText: string, maxTokens: number): Promise<string>;
@@ -1336,6 +1371,8 @@ export declare function summarizeTerse(items: Array<{
1336
1371
  role: string;
1337
1372
  text: string;
1338
1373
  }>, targetTokens: number, llm: LlmComplete, session?: string): Promise<EscalationResult>;
1374
+ /** Same budget the ladder falls back to, so heal sees one shape. */
1375
+ export declare const LEVEL3_TOKENS = 512;
1339
1376
  export declare function deterministicTruncate(text: string, targetTokens: number): string;
1340
1377
  export interface SummarizerConfig {
1341
1378
  /** One model id, or an ordered fallback chain. `auto` is the session model. */
@@ -1439,7 +1476,32 @@ export interface SummarizerOptions {
1439
1476
  readonly summarizerTokensPerPass?: number;
1440
1477
  /** Tokens one on-demand heal run may spend across all of its calls. */
1441
1478
  readonly summarizerTokensPerHeal?: number;
1442
- }
1479
+ /** Fires at compaction time only; ingest still stores every message verbatim. */
1480
+ readonly mechanicalTools?: readonly string[];
1481
+ }
1482
+ /** Empty default: callers opt in and existing passes keep their spend. */
1483
+ export declare const DEFAULT_MECHANICAL_TOOLS: readonly string[];
1484
+ export declare function normalizeMechanicalTools(tools: readonly string[] | undefined): string[];
1485
+ /** The tool name a message carries, if any. Reads both hosts: `tool <name>`
1486
+ * (OpenCode), `[tool:<name>]` (core), `tool-call: <name>`, plus a JSON
1487
+ * payload that names the call. Lowercased, else undefined.
1488
+ *
1489
+ * A marker only counts at a block start: `entryToText` joins content blocks
1490
+ * with newlines, so a genuine call marker opens a line while prose quoting one
1491
+ * sits mid-line. The caller may pass the stored role: `user` and `custom` rows
1492
+ * never carry calls, and an `assistant` row mixing prose with a call keeps the
1493
+ * prose on the summarizer path (tool-only rows still skip). */
1494
+ export declare function mechanicalToolNameOf(message: {
1495
+ text: string;
1496
+ payload?: string;
1497
+ role?: string;
1498
+ }): string | undefined;
1499
+ /** Normalized here, so callers may pass raw config values. */
1500
+ export declare function isMechanicalToolMessage(message: {
1501
+ text: string;
1502
+ payload?: string;
1503
+ role?: string;
1504
+ }, tools: readonly string[]): boolean;
1443
1505
  /** The engine config for these options. An unresolved budget keeps the core
1444
1506
  * default, which is also what a direct caller that passes no options gets. */
1445
1507
  export declare function summarizerConfig(options: SummarizerOptions): SummarizerConfig;
@@ -1674,6 +1736,8 @@ export interface CompactionInput {
1674
1736
  targetTokens?: number;
1675
1737
  /** Where the returned summary must reach back to; defaults to the span start. */
1676
1738
  frontierFrom?: string;
1739
+ /** Read at compaction time only; ingest stores every message verbatim. */
1740
+ mechanicalTools?: readonly string[];
1677
1741
  }
1678
1742
  export type CompactionOutcome = {
1679
1743
  kind: "stored";
@@ -1681,6 +1745,10 @@ export type CompactionOutcome = {
1681
1745
  leafSummaryIds: number[];
1682
1746
  condensedSummaryIds: number[];
1683
1747
  ingested: number;
1748
+ /** Leaves stored without a model call on the configured mechanical path. */
1749
+ mechanicalLeaves?: number;
1750
+ /** Condensed nodes built without a model call from mechanical children. */
1751
+ mechanicalCondensed?: number;
1684
1752
  } |
1685
1753
  /** No text, and why. A span another live run claimed, a span an existing node
1686
1754
  * already covers, and a span the store holds no row for are all states a pass
@@ -1705,6 +1773,8 @@ export declare function runCompaction(store: PassStore, input: CompactionInput,
1705
1773
  session?: string;
1706
1774
  maxDepth?: number;
1707
1775
  stage?: CompactionStage;
1776
+ /** Empty keeps the LLM path. */
1777
+ mechanicalTools?: readonly string[];
1708
1778
  }): Promise<CompactionOutcome>;
1709
1779
  /** The next group to condense: the oldest run of adjacent same-depth nodes, at
1710
1780
  * the shallowest depth with a run of two or more, capped at MAX_DAG_FANOUT.
@@ -1820,7 +1890,7 @@ export interface CompactionBounds {
1820
1890
  * not message text, so bash and branch-summary rows align. An unknown
1821
1891
  * firstKeptEntryId yields an empty span, so the caller falls back to Pi. */
1822
1892
  export declare function resolveCompactionSpan(branchMsgs: readonly AlignedEntry[], bounds: CompactionBounds): AlignedEntry[];
1823
- export type FindingKind = "fts-index" | "summary-span" | "summary-shape" | "orphan-child" | "duplicate-span" | "foreign-key" | "schema-version" | "dead-run" | "coverage";
1893
+ export type FindingKind = "fts-index" | "summary-span" | "summary-shape" | "orphan-child" | "duplicate-span" | "foreign-key" | "schema-version" | "dead-run" | "orphan-sidecar" | "coverage";
1824
1894
  export interface Finding {
1825
1895
  kind: FindingKind;
1826
1896
  /** `violation` is an invariant the DAG depends on; `coverage` is a count. */
@@ -1834,7 +1904,7 @@ export interface IntegrityReport {
1834
1904
  checks: number;
1835
1905
  findings: Finding[];
1836
1906
  }
1837
- export declare function checkIntegrity(store: LcmStore): IntegrityReport;
1907
+ export declare function checkIntegrity(store: LcmStore, opts?: IntegritySnapshotOptions): IntegrityReport;
1838
1908
  /** Fix what can be fixed without guessing: the derived index, rows whose span no
1839
1909
  * longer exists, nodes whose child no longer resolves, rows a run whose writer is
1840
1910
  * gone left behind, and predated duplicate spans. An inverted span is left
@@ -2328,6 +2398,40 @@ export type ViewResult = {
2328
2398
  ok: false;
2329
2399
  text: string;
2330
2400
  };
2401
+ declare const LCM_ROLES: readonly ["user", "assistant", "toolResult", "custom"];
2402
+ type GrepFilter = {
2403
+ role?: (typeof LCM_ROLES)[number];
2404
+ dateStart?: number;
2405
+ dateEnd?: number;
2406
+ toolName?: string;
2407
+ };
2408
+ /** The `[tool:Name]` marker in front of a tool call's arguments, so a scoped
2409
+ * search needs no table or index. */
2410
+ export declare function toolMarker(toolName: string): string;
2411
+ /** Quote a string as one FTS5 phrase. Embedded quotes double; JSON.stringify
2412
+ * emits backslash escapes which FTS5 rejects. */
2413
+ export declare function ftsPhrase(text: string): string;
2414
+ /** Rewrite an FTS5 query to its tool-scoped form. The marker passes as a quoted
2415
+ * phrase, which the FTS5 tokenizer splits the same way as the stored text, so
2416
+ * prose with the same adjacent words (for example "which tool read the file")
2417
+ * can also match. Anchoring to the row start would miss mixed blocks because
2418
+ * entryToText appends blocks in order, so the scoped search keeps this broader
2419
+ * match and callers read the hit to confirm the tool. */
2420
+ export declare function withToolScope(query: string, toolName: string): string;
2421
+ /** Row ids a tool scope covers: the call rows that name the tool plus the
2422
+ * toolResult rows that follow them. Pi stores a call and its result in
2423
+ * separate rows, so a same-row `query AND marker` FTS misses a word found
2424
+ * only in the result. A trailing run of toolResult rows after a marker row
2425
+ * belongs to that call batch; any other role ends the run, as does a
2426
+ * toolResult row that names a different tool. Bracket text is checked
2427
+ * directly so one row naming two tools anchors both, and
2428
+ * mechanicalToolNameOf covers the `tool <name>` and payload forms. User and
2429
+ * custom rows never open a scope, so quoting a marker in chat pulls in no
2430
+ * results; a mixed assistant block still opens through its literal marker. */
2431
+ export declare function toolScopeMessageIds(store: LcmStore, toolName: string, opts?: {
2432
+ includeRemoved?: boolean;
2433
+ withinMessageIds?: ReadonlySet<number>;
2434
+ }): Set<number>;
2331
2435
  export declare function grepView(store: LcmStore, redact: (text: string) => string, params: {
2332
2436
  query?: string;
2333
2437
  pattern?: string;
@@ -2342,17 +2446,37 @@ export declare function grepView(store: LcmStore, redact: (text: string) => stri
2342
2446
  now?: () => number;
2343
2447
  /** Tool the continuation marker names. Defaults to `lcm_expand_query`. */
2344
2448
  expandTool?: string;
2449
+ role?: GrepFilter["role"];
2450
+ dateStart?: number;
2451
+ dateEnd?: number;
2452
+ /** Query searches only; with a pattern it is refused. */
2453
+ toolName?: string;
2345
2454
  }): ViewResult;
2455
+ /** A scoped FTS search that stays complete. `store.grep` applies its SQL
2456
+ * LIMIT before it intersects the id set, so one bounded call can miss rows
2457
+ * that rank past the window. The unscoped count bounds the rank order, so one
2458
+ * fetch of that size holds every in-scope match and the caller pages the
2459
+ * filtered set. Rank order is preserved. Throws on invalid FTS5 syntax, like
2460
+ * `store.grep`, so callers keep their quoted-phrase fallback. */
2461
+ export declare function scopedGrepAll(store: LcmStore, pattern: string, opts: {
2462
+ includeRemoved: boolean;
2463
+ role?: GrepFilter["role"];
2464
+ dateStart?: number;
2465
+ dateEnd?: number;
2466
+ scoped: ReadonlySet<number>;
2467
+ }): ReturnType<LcmStore["grep"]>;
2346
2468
  export declare function describeView(store: LcmStore, redact: (text: string) => string, id: number): ViewResult;
2347
- /** List the summary's messages, or read one of them as a window of characters.
2348
- * A message read without a summary names an entry directly, which is how an
2349
- * entry that no leaf covers yet is read at all. */
2469
+ /** List a summary's messages, or read one as a window. Without a summary an
2470
+ * entry id reads directly; with `anchor` and offset 0 the window centers on
2471
+ * that entry, clamped at the span edges. A nonzero offset with `anchor` is an
2472
+ * absolute message index, so the next page starts from the reported offset. */
2350
2473
  export type ExpandParams = {
2351
2474
  mode: "list";
2352
2475
  id: number;
2353
2476
  offset: number;
2354
2477
  limit: number;
2355
2478
  maxChars: number;
2479
+ anchor?: string;
2356
2480
  } | {
2357
2481
  mode: "message";
2358
2482
  id?: number;
@@ -2361,7 +2485,20 @@ export type ExpandParams = {
2361
2485
  maxChars: number;
2362
2486
  };
2363
2487
  export declare function expandView(store: LcmStore, redact: (text: string) => string, params: ExpandParams): ViewResult;
2364
- export declare function historyManifest(store: LcmStore, limit: number): string;
2488
+ /** Sort headers the caller already holds, so FTS plus regex stay the recall
2489
+ * path and no score is stored. Ties keep input order. */
2490
+ export declare function orderSummariesByOverlap<T extends {
2491
+ text: string;
2492
+ }>(query: string, nodes: readonly T[]): T[];
2493
+ /** At or above this length with no linked file a body reads as `large-inline`,
2494
+ * matching the ingest default, so view and writer agree. */
2495
+ export declare const LARGE_INLINE_CHARS = 32000;
2496
+ /** A read-only files manifest, built at read time, so nothing is stored and
2497
+ * no model is called. */
2498
+ export declare function filesView(store: LcmStore, redact: (text: string) => string, opts?: {
2499
+ summaryId?: number;
2500
+ }): ViewResult;
2501
+ export declare function historyManifest(store: LcmStore, limit: number, query?: string): string;
2365
2502
  /** Redactor that re-reads the flag on every call, so a cockpit toggle takes
2366
2503
  * effect immediately: a getter, because patchConfig replaces the config object
2367
2504
  * and closing over it would pin the session-start value. */
@@ -2508,6 +2645,10 @@ export declare function searchSessions(refs: readonly SessionRef[], query: strin
2508
2645
  deadlineMs?: number;
2509
2646
  perSession?: number;
2510
2647
  now?: () => number;
2648
+ role?: "user" | "assistant" | "toolResult" | "custom";
2649
+ dateStart?: number;
2650
+ dateEnd?: number;
2651
+ toolName?: string;
2511
2652
  }): SessionSearchResult;
2512
2653
  export declare const SEARCH_SCOPE_HELP = "session (default) searches this session. sessions searches past sessions in this project's working directory. all_sessions searches past sessions in every project. A cross-session hit is a pointer labelled [lcm:session <hash>] that lcm_expand_query({session, entry_id}) reads, so a past session's text is never injected into the context on its own.";
2513
2654
  /** The cross-session hits as one labelled block, plus what the scan did not read.