@mrdrmccoy/xerj-opencode-plugin 0.4.4 → 0.4.5

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.4",
3
+ "version": "0.4.5",
4
4
  "description": "XERJ integration for OpenCode: local code and docs search, agent memory, and retrieval accounting.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -47,6 +47,9 @@ const BACKOFF_MS = [1_000, 2_000, 5_000, 15_000, 30_000] as const;
47
47
  const MAX_RESTARTS = 5;
48
48
  const STOP_GRACE_MS = 3_000;
49
49
  const STDERR_TAIL_LINES = 20;
50
+ // The engine refuses a second watcher for the same journal and says so.
51
+ const CONTENTION_MARKER = "already owns";
52
+ const CONTENTION_RETRY_MS = 5_000;
50
53
 
51
54
  export class CorpusService {
52
55
  readonly layout: CorpusLayout;
@@ -62,6 +65,7 @@ export class CorpusService {
62
65
  private timer: ReturnType<typeof setInterval> | null = null;
63
66
  private stopped = false;
64
67
  private spawning = false;
68
+ private lastStderr = "";
65
69
 
66
70
  constructor(private readonly options: CorpusServiceOptions) {
67
71
  this.layout = corpusLayout(options.projectRoot, options.prefix, options.dataRoot);
@@ -160,6 +164,7 @@ export class CorpusService {
160
164
 
161
165
  this.child = child;
162
166
  this.spawning = false;
167
+ this.lastStderr = "";
163
168
  if (watch) this.state = "starting";
164
169
 
165
170
  this.attachOutput(child);
@@ -204,8 +209,9 @@ export class CorpusService {
204
209
  });
205
210
 
206
211
  child.once("close", () => {
212
+ this.lastStderr = stderr.join("").slice(-2000);
207
213
  if (stderr.length > 0) {
208
- this.options.logger.debug("autoindex stderr", stderr.join("").slice(-2000));
214
+ this.options.logger.debug("autoindex stderr", this.lastStderr);
209
215
  }
210
216
  });
211
217
  }
@@ -213,6 +219,19 @@ export class CorpusService {
213
219
  private handleWatchExit(code: number | null, signal: NodeJS.Signals | null): void {
214
220
  const meaning = exitMeaning(code);
215
221
 
222
+ // Another process owns this journal, typically the plugin instance of a
223
+ // server that is being replaced. Ownership frees when that process exits,
224
+ // so wait it out instead of counting a failure or burning the restart
225
+ // budget.
226
+ if (this.lastStderr.includes(CONTENTION_MARKER)) {
227
+ this.state = "starting";
228
+ this.lastError = null;
229
+ this.options.logger.debug("autoindex watcher journal is owned elsewhere; retrying");
230
+ this.options.onChange?.();
231
+ setTimeout(() => this.spawn(true), CONTENTION_RETRY_MS);
232
+ return;
233
+ }
234
+
216
235
  // A clean exit from `--watch` is unexpected; it should stay resident.
217
236
  if (meaning === "decision") {
218
237
  this.state = "idle";
package/src/index.ts CHANGED
@@ -27,6 +27,7 @@ import {
27
27
  environmentFrom,
28
28
  externalPrefix,
29
29
  indexPrefix,
30
+ isOverbroadRoot,
30
31
  memoryNamespace,
31
32
  projectStateDir,
32
33
  webPrefix,
@@ -81,6 +82,14 @@ export default Plugin.define({
81
82
  // The location may be a subdirectory of the project; index the project root
82
83
  // so a session opened in `docs/` does not create a second corpus.
83
84
  const projectRoot = ctx.location.project.canonical;
85
+ const overbroad = isOverbroadRoot(projectRoot);
86
+ const indexWarnings = [...warnings];
87
+ if (overbroad) {
88
+ const refusal =
89
+ `refusing to index ${projectRoot}: the home directory and filesystem roots are too broad. Open OpenCode inside a project.`;
90
+ indexWarnings.push(refusal);
91
+ logger.warn(refusal);
92
+ }
84
93
  const prefix = indexPrefix(projectRoot);
85
94
  const brains = brainSpecs(projectRoot, config.index.brain);
86
95
  const hiddenRoots = documentRoots(projectRoot, config.index.hidden);
@@ -99,7 +108,7 @@ export default Plugin.define({
99
108
  hiddenRoots,
100
109
  anomaly: null,
101
110
  error: null,
102
- warnings: [...warnings],
111
+ warnings: indexWarnings,
103
112
  };
104
113
 
105
114
  const ledger = new Ledger({
@@ -502,7 +511,7 @@ export default Plugin.define({
502
511
  .catch(() => {});
503
512
  },
504
513
  });
505
- await runtime.corpus.start();
514
+ if (!overbroad) await runtime.corpus.start();
506
515
 
507
516
  if (runtime.hiddenRoots.length > 0) {
508
517
  runtime.hidden = new HiddenIndex({
@@ -521,7 +530,7 @@ export default Plugin.define({
521
530
  },
522
531
  });
523
532
 
524
- if (config.index.auto) await runtime.hidden.start();
533
+ if (config.index.auto && !overbroad) await runtime.hidden.start();
525
534
  }
526
535
 
527
536
  if (runtime.brains.length > 0) {
@@ -544,7 +553,7 @@ export default Plugin.define({
544
553
 
545
554
  // One-shot: graph indexing cannot run in watch mode. `index.auto`
546
555
  // gates startup runs only; an explicit reindex still works.
547
- if (config.index.auto) void runtime.brain.runAll();
556
+ if (config.index.auto && !overbroad) void runtime.brain.runAll();
548
557
  }
549
558
 
550
559
  await registration.events.emit("updated", { updatedAt: Date.now() });
package/src/paths.ts CHANGED
@@ -12,7 +12,6 @@ export interface Environment {
12
12
  readonly HOME?: string;
13
13
  }
14
14
 
15
- /** Picks the variables that matter out of a wider process environment. */
16
15
  export function environmentFrom(env: Readonly<Record<string, string | undefined>>): Environment {
17
16
  return {
18
17
  XDG_DATA_HOME: env.XDG_DATA_HOME,
@@ -21,6 +20,22 @@ export function environmentFrom(env: Readonly<Record<string, string | undefined>
21
20
  };
22
21
  }
23
22
 
23
+ /**
24
+ * True when a directory is too broad to index: the home directory itself or a
25
+ * filesystem root. Indexing it walks every cache, download and trash folder and
26
+ * costs minutes of I/O for results nobody asked for.
27
+ */
28
+ export function isOverbroadRoot(
29
+ directory: string,
30
+ home: string = homedir(),
31
+ platform: NodeJS.Platform = process.platform,
32
+ ): boolean {
33
+ const resolved = resolve(directory);
34
+ const root = platform === "win32" ? resolve(resolved, sep) : "/";
35
+ return resolved === root || resolved === resolve(home);
36
+ }
37
+
38
+
24
39
  const APP_DIR = "opencode/xerj";
25
40
  const PREFIX_ROOT = "xop";
26
41
  const MAX_SLUG_LENGTH = 24;
package/src/rpc.ts CHANGED
@@ -247,4 +247,4 @@ const definition = {
247
247
 
248
248
  export const XerjRpc = Rpc.define(definition);
249
249
 
250
- export const PLUGIN_VERSION = "0.4.4";
250
+ export const PLUGIN_VERSION = "0.4.5";