dsh-plugin-codegraph-tree-sitter 0.6.0 → 0.6.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.
package/lib/index.js CHANGED
@@ -146,6 +146,10 @@ export function apply(ctx, config) {
146
146
  // legitimately trigger another refresh. The map only keeps a burst of sessions opened at once from
147
147
  // queueing the same rebuild repeatedly; the freshness decision itself reads the status sidecar.
148
148
  const lastAutoRun = new Map();
149
+ // Roots whose background run is still executing. Kept apart from `lastAutoRun`, which is a time
150
+ // window: a first build can outlive the window, and two builds on one root would only race the same
151
+ // index files.
152
+ const autoIndexInFlight = new Set();
149
153
  // `agent/created` is typed by `@deepseek-ai/dsh-agent`'s module augmentation. Reading the event
150
154
  // through a local structural face keeps this package's host dependencies unchanged: it needs one
151
155
  // event name and one payload shape, not the whole agent package.
@@ -210,13 +214,11 @@ export function apply(ctx, config) {
210
214
  if (resolved.autoIndexOnSession) {
211
215
  // Registered inside this effect so the listener follows the plugin's lifetime: unloading the
212
216
  // plugin must stop new sessions from scheduling work this instance can no longer run.
213
- yield agentEvents.on('agent/created', payload => autoIndexOnSession(ctx, resolved, lastAutoRun, payload, ensureWatching));
217
+ yield agentEvents.on('agent/created', payload => autoIndexOnSession(ctx, resolved, lastAutoRun, autoIndexInFlight, payload, ensureWatching));
214
218
  }
215
219
  yield ctx.codegraph.registerIndexer(indexer);
216
220
  }, 'codegraph-tree-sitter');
217
221
  }
218
- /** Stable job kind of an automatic session-start index, used for retention and diagnostics. */
219
- const AUTO_INDEX_JOB_KIND = 'codegraph-index';
220
222
  /**
221
223
  * Whether a directory is the harness's own home rather than a project checkout.
222
224
  *
@@ -235,19 +237,28 @@ function isHarnessHome(root) {
235
237
  * is there has gone stale.
236
238
  *
237
239
  * Runs synchronously inside `agent/created` dispatch, so it must never block or throw: it only decides
238
- * whether a build is warranted and hands one to the host's job registry. A synchronous build here
239
- * would stall agent creation for the length of a parse, which is why a composition without background
240
- * jobs does nothing instead.
240
+ * whether a build is warranted and starts one in the background. A synchronous build here would stall
241
+ * agent creation for the length of a parse.
242
+ *
243
+ * The run deliberately does **not** go through the host's job registry. A registered job reports its
244
+ * settlement back to the owning agent: the host injects a completion notice into the next step and,
245
+ * under its default `wakeup` delivery, opens a turn on an idle one. An automatic index nobody asked
246
+ * for must not appear in the transcript or start a reply of its own, so this runs as a plain
247
+ * background promise. The explicit `codegraph_index` tool still registers a job — there a caller is
248
+ * waiting for the id.
241
249
  *
242
250
  * Refreshing a stale index is what restarts the file watcher after a host restart: watching only
243
251
  * begins once a successful index has established a baseline, so a workspace whose index is minutes
244
252
  * old would otherwise stay frozen at whatever the last process left behind.
245
- * @param ctx - the plugin context, consulted for the optional job registry.
253
+ * @param ctx - the plugin context.
246
254
  * @param config - the resolved plugin configuration, for the staleness threshold.
247
255
  * @param lastAutoRun - when this instance last started an automatic index for each root.
256
+ * @param autoIndexInFlight - roots whose background run is still executing, so a slow build cannot be
257
+ * started twice by sessions that open past the freshness window.
248
258
  * @param payload - the `agent/created` dispatch payload.
259
+ * @param ensureWatching - starts file watching for a workspace that is already current.
249
260
  */
250
- function autoIndexOnSession(ctx, config, lastAutoRun, payload, ensureWatching) {
261
+ function autoIndexOnSession(ctx, config, lastAutoRun, autoIndexInFlight, payload, ensureWatching) {
251
262
  try {
252
263
  const agent = payload.agent;
253
264
  const root = agent?.session?.header?.cwd;
@@ -266,9 +277,12 @@ function autoIndexOnSession(ctx, config, lastAutoRun, payload, ensureWatching) {
266
277
  const previous = lastAutoRun.get(root);
267
278
  if (previous !== undefined && now - previous < config.autoRefreshAfterMs)
268
279
  return;
280
+ // A build already running for this root is the one the window would have deduplicated had it been
281
+ // slow enough to straddle it; starting a second would only race the same index files.
282
+ if (autoIndexInFlight.has(root))
283
+ return;
269
284
  // A fresh sidecar means a recent run is already describing this workspace; an absent, unreadable
270
- // or old one means the graph is missing or behind. Checked before the job lookup so a composition
271
- // without background jobs never pays for the probe.
285
+ // or old one means the graph is missing or behind.
272
286
  const indexedAt = readStatusIndexedAt(root);
273
287
  if (indexedAt !== undefined && now - indexedAt < config.autoRefreshAfterMs) {
274
288
  // The graph is current, so no build is warranted — but watching is process-local state and
@@ -278,21 +292,10 @@ function autoIndexOnSession(ctx, config, lastAutoRun, payload, ensureWatching) {
278
292
  ensureWatching(root);
279
293
  return;
280
294
  }
281
- // `ctx.get` rather than an `inject` entry: `jobs` is a host composition choice, and requiring it in
282
- // `inject` would hide this whole indexer from a profile that runs no background jobs.
283
- const jobs = ctx.get('jobs');
284
- if (jobs === undefined)
285
- return;
286
- jobs.start({
287
- // The registry keys a job by its owning session id, not the agent object.
288
- owner: agent.id,
289
- kind: AUTO_INDEX_JOB_KIND,
290
- label: `codegraph auto index ${root}`,
291
- run: handle => runAutoIndexJob(ctx, root, config, handle),
292
- });
293
- // Marked only after the registry accepted the job, so a transient refusal leaves the workspace
294
- // eligible for the next session that opens in it.
295
295
  lastAutoRun.set(root, now);
296
+ autoIndexInFlight.add(root);
297
+ // Never rejects: a lost index is a degradation, not an error the session can act on.
298
+ void runAutomaticIndex(ctx, root, config).finally(() => autoIndexInFlight.delete(root));
296
299
  }
297
300
  catch (error) {
298
301
  // The host treats a throwing listener as vetoing publication. Losing an index is a degradation,
@@ -300,6 +303,33 @@ function autoIndexOnSession(ctx, config, lastAutoRun, payload, ensureWatching) {
300
303
  console.error(`codegraph-tree-sitter: automatic indexing for a new session failed: ${String(error)}`);
301
304
  }
302
305
  }
306
+ /**
307
+ * Run one automatic index to completion, reporting a failure instead of propagating it.
308
+ *
309
+ * The size guard stays: parsing a workspace past `autoIndexMaxFiles` would exhaust the WASM heap and
310
+ * take the desktop down with it, and an automatic index is a convenience. Skipping it is a decision,
311
+ * not a failure, so it is logged once and the session continues untouched.
312
+ * @param ctx - the plugin context.
313
+ * @param root - absolute workspace path.
314
+ * @param config - the resolved plugin configuration.
315
+ */
316
+ async function runAutomaticIndex(ctx, root, config) {
317
+ const controller = new AbortController();
318
+ try {
319
+ const oversized = await exceedsAutoIndexLimit(root, config, controller.signal);
320
+ if (oversized !== undefined) {
321
+ console.error(`codegraph-tree-sitter: skipped automatic indexing of ${root}: it holds at least ${String(oversized)} ` +
322
+ `candidate source files, past the ${String(config.autoIndexMaxFiles)}-file session-start limit. ` +
323
+ 'Run codegraph_index yourself if you want a graph for it.');
324
+ return;
325
+ }
326
+ // A successful index is what starts file watching, inside `indexer.index`.
327
+ await ctx.codegraph.index(root, controller.signal);
328
+ }
329
+ catch (error) {
330
+ console.error(`codegraph-tree-sitter: automatic index for ${root} failed: ${String(error)}`);
331
+ }
332
+ }
303
333
  /**
304
334
  * How many candidate source files `root` holds, when that is past the session-start limit.
305
335
  *
@@ -365,46 +395,6 @@ function readStatusIndexedAt(root) {
365
395
  return undefined;
366
396
  }
367
397
  }
368
- /**
369
- * The producer body of one automatic indexing job.
370
- *
371
- * Both halves of the host's job contract are mandatory: `cancel` aborts the run, and `done` settles
372
- * even on failure — a rejected `done` breaks the registry's bookkeeping and leaves the job looking
373
- * stuck forever.
374
- * @param ctx - the plugin context.
375
- * @param root - the workspace to index.
376
- * @param handle - the job's producer face, for the job's live log.
377
- * @returns the job's cancellation hook and settlement promise.
378
- */
379
- function runAutoIndexJob(ctx, root, config, handle) {
380
- const controller = new AbortController();
381
- const done = (async () => {
382
- try {
383
- const oversized = await exceedsAutoIndexLimit(root, config, controller.signal);
384
- if (oversized !== undefined) {
385
- // Declining here is the whole point of the check: parsing this workspace would exhaust the
386
- // WASM heap and take the desktop down with it, and an automatic index is a convenience.
387
- handle.append(`Skipped automatic indexing of ${root}: it holds at least ${String(oversized)} candidate ` +
388
- `source files, past the ${String(config.autoIndexMaxFiles)}-file session-start limit. ` +
389
- `Run codegraph_index yourself if you want a graph for it.\n`);
390
- // `completed`, not `failed`: nothing went wrong. The guard decided this workspace is not a
391
- // candidate for an automatic run, and a user reading the job list must be able to tell that
392
- // apart from a build that actually broke.
393
- return { status: 'completed', detail: 'skipped: workspace too large for automatic indexing' };
394
- }
395
- const report = await ctx.codegraph.index(root, controller.signal);
396
- handle.append(`Indexed ${root}: ${report.filesIndexed} files, ${report.nodeCount} symbols, ` +
397
- `${report.edgeCount} relationships.\n`);
398
- return { status: 'completed' };
399
- }
400
- catch (error) {
401
- const message = String(error);
402
- handle.append(`codegraph auto index failed for ${root}: ${message}\n`);
403
- return { status: 'failed', detail: message };
404
- }
405
- })();
406
- return { cancel: () => controller.abort(), done };
407
- }
408
398
  /**
409
399
  * Run one watch-triggered reindex. Always a full rebuild in this version — `runIndex` has no notion of
410
400
  * a file subset — so the watcher's own collected paths are not consulted here.
package/lib/schema.js CHANGED
@@ -144,7 +144,12 @@ export async function writeGraph(databasePath, input) {
144
144
  */
145
145
  async function writeStatusFile(directory, status) {
146
146
  const target = join(directory, basename(INDEX_STATUS_RELATIVE_PATH));
147
- const temp = `${target}.tmp`;
147
+ // Unique like the graph's own staging name, and for the same reason: two runs on one workspace are
148
+ // allowed to overlap (a session-start build and an explicit `codegraph_index`, or a watcher rebuild),
149
+ // and a shared fixed-name `.tmp` makes the loser's `rename` fail with ENOENT after the winner moved
150
+ // it — which turned a harmless duplicate run into a failed tool call. Last rename wins; both graphs
151
+ // are complete.
152
+ const temp = `${target}.tmp-${process.pid}-${randomUUID()}`;
148
153
  await writeFile(temp, JSON.stringify(status));
149
154
  await rename(temp, target);
150
155
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-codegraph-tree-sitter",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Self-built code-graph indexer for DeepSeek Harness — parses a workspace with web-tree-sitter and writes schema version 4 to .codegraph/codegraph.db, the same on-disk format the codegraph CLI writes, so no external indexer is required",
5
5
  "keywords": [
6
6
  "dsh",
@@ -51,7 +51,7 @@
51
51
  "@deepseek-ai/schemastery": "^3.18.1",
52
52
  "tree-sitter-wasms": "^0.1.13",
53
53
  "web-tree-sitter": "^0.25.10",
54
- "dsh-plugin-codegraph-service": "^0.6.0"
54
+ "dsh-plugin-codegraph-service": "^0.6.1"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "@deepseek-ai/cordis": ">=4.0.1",
@@ -59,11 +59,11 @@
59
59
  "@deepseek-ai/dsh-invariants": ">=0.1.0-rc.6"
60
60
  },
61
61
  "optionalDependencies": {
62
- "dsh-plugin-codegraph-indexer-darwin-arm64": "0.6.0",
63
- "dsh-plugin-codegraph-indexer-darwin-x64": "0.6.0",
64
- "dsh-plugin-codegraph-indexer-linux-x64-gnu": "0.6.0",
65
- "dsh-plugin-codegraph-indexer-linux-arm64-gnu": "0.6.0",
66
- "dsh-plugin-codegraph-indexer-win32-x64-msvc": "0.6.0"
62
+ "dsh-plugin-codegraph-indexer-darwin-arm64": "0.6.1",
63
+ "dsh-plugin-codegraph-indexer-darwin-x64": "0.6.1",
64
+ "dsh-plugin-codegraph-indexer-linux-x64-gnu": "0.6.1",
65
+ "dsh-plugin-codegraph-indexer-linux-arm64-gnu": "0.6.1",
66
+ "dsh-plugin-codegraph-indexer-win32-x64-msvc": "0.6.1"
67
67
  },
68
68
  "devDependencies": {
69
69
  "@deepseek-ai/cordis": "^4.0.1",