@mrdrmccoy/xerj-opencode-plugin 0.4.5 → 0.4.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrdrmccoy/xerj-opencode-plugin",
3
- "version": "0.4.5",
3
+ "version": "0.4.6",
4
4
  "description": "XERJ integration for OpenCode: local code and docs search, agent memory, and retrieval accounting.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -155,6 +155,7 @@ async function retrieve(
155
155
 
156
156
  try {
157
157
  return await search.search(text, {
158
+ surface: "context",
158
159
  k: options.config.context.maxPassages,
159
160
  maxTokens: options.config.context.maxTokens,
160
161
  signal: controller.signal,
@@ -50,6 +50,10 @@ const STDERR_TAIL_LINES = 20;
50
50
  // The engine refuses a second watcher for the same journal and says so.
51
51
  const CONTENTION_MARKER = "already owns";
52
52
  const CONTENTION_RETRY_MS = 5_000;
53
+ // A watcher that survives this long is watching, even before its first run
54
+ // document lands: an initial pass over a large corpus takes minutes, and the
55
+ // status would otherwise read "starting" the whole time.
56
+ const WATCH_SETTLE_MS = 3_000;
53
57
 
54
58
  export class CorpusService {
55
59
  readonly layout: CorpusLayout;
@@ -66,6 +70,7 @@ export class CorpusService {
66
70
  private stopped = false;
67
71
  private spawning = false;
68
72
  private lastStderr = "";
73
+ private spawnedAt = 0;
69
74
 
70
75
  constructor(private readonly options: CorpusServiceOptions) {
71
76
  this.layout = corpusLayout(options.projectRoot, options.prefix, options.dataRoot);
@@ -128,8 +133,13 @@ export class CorpusService {
128
133
  }
129
134
 
130
135
  snapshot(): CorpusSnapshot {
136
+ const settled =
137
+ this.state === "starting" &&
138
+ this.child !== null &&
139
+ Date.now() - this.spawnedAt >= WATCH_SETTLE_MS;
140
+
131
141
  return {
132
- state: this.state,
142
+ state: settled ? "watching" : this.state,
133
143
  lastRun: this.lastRun,
134
144
  restarts: this.restarts,
135
145
  runs: this.runs,
@@ -165,6 +175,7 @@ export class CorpusService {
165
175
  this.child = child;
166
176
  this.spawning = false;
167
177
  this.lastStderr = "";
178
+ this.spawnedAt = Date.now();
168
179
  if (watch) this.state = "starting";
169
180
 
170
181
  this.attachOutput(child);
@@ -59,3 +59,23 @@ export function demoteGenerated(
59
59
  })
60
60
  .sort((left, right) => right.score - left.score);
61
61
  }
62
+
63
+ /**
64
+ * Drops generated files the query does not name. Injection shares one small
65
+ * budget across a whole turn, and a lockfile passage matches on dependency
66
+ * names that have nothing to do with the question, so it can crowd out the
67
+ * source that answers it. Tool searches keep them: the caller asked directly.
68
+ */
69
+ export function dropGenerated(
70
+ hits: ReadonlyArray<ParsedHit>,
71
+ query: string,
72
+ ): ParsedHit[] {
73
+ const named = query.toLowerCase();
74
+
75
+ return hits.filter((hit) => {
76
+ if (hit.path === null) return true;
77
+ if (!isGeneratedPath(hit.path)) return true;
78
+ const basename = hit.path.toLowerCase().split("/").pop() ?? "";
79
+ return named.includes(basename);
80
+ });
81
+ }
@@ -14,7 +14,7 @@ import type { Logger } from "../log.ts";
14
14
  import { XerjClient } from "../node/client.ts";
15
15
  import type { BrainSpec } from "../paths.ts";
16
16
  import type { BrainGraph } from "./brains.ts";
17
- import { demoteGenerated } from "./rank.ts";
17
+ import { demoteGenerated, dropGenerated } from "./rank.ts";
18
18
 
19
19
  export interface SearchHit {
20
20
  readonly index: string;
@@ -36,7 +36,14 @@ export interface SearchResult {
36
36
  readonly narrowingBytes: number;
37
37
  }
38
38
 
39
+ /**
40
+ * Where the results go. Injection shares a small budget across a whole turn,
41
+ * so it is stricter than an explicit tool call about generated files.
42
+ */
43
+ export type SearchSurface = "context" | "tool";
44
+
39
45
  export interface SearchOptions {
46
+ readonly surface?: SearchSurface;
40
47
  readonly k?: number;
41
48
  readonly maxTokens?: number;
42
49
  readonly signal?: AbortSignal;
@@ -132,7 +139,8 @@ export class SearchService {
132
139
 
133
140
  const strategy = semantic ? "hybrid" : "lexical";
134
141
  const fused = semantic ? fuse([lexical.hits, semantic.hits], k) : lexical.hits.slice(0, k);
135
- const located = demoteGenerated(this.locateBrainPaths(fused), trimmed);
142
+ const ranked = demoteGenerated(this.locateBrainPaths(fused), trimmed);
143
+ const located = options.surface === "context" ? dropGenerated(ranked, trimmed) : ranked;
136
144
  const resolved = await this.resolveLines(located, trimmed);
137
145
  const hits = dropShadowedWholeFiles(
138
146
  resolved,