dsh-plugin-codegraph-tree-sitter 0.5.0 → 0.5.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.d.ts CHANGED
@@ -79,6 +79,8 @@ export declare const DEFAULT_CONCURRENCY = 4;
79
79
  export declare const DEFAULT_WATCH_DEBOUNCE_MS = 2000;
80
80
  /** Default cap on directories watched individually, one watch each, by the fs seam. */
81
81
  export declare const DEFAULT_MAX_WATCHED_DIRECTORIES = 4000;
82
+ /** Default age past which a session-open refreshes an existing index (ten minutes). */
83
+ export declare const DEFAULT_AUTO_REFRESH_AFTER_MS: number;
82
84
  /** Plugin configuration: the indexer's identity and every deployment-varying bound on a run. */
83
85
  export interface Config {
84
86
  /**
@@ -174,6 +176,13 @@ export interface Config {
174
176
  * so a composition without background jobs simply schedules nothing.
175
177
  */
176
178
  autoIndexOnSession?: boolean;
179
+ /**
180
+ * How stale an existing index may be before a new session refreshes it in the background (default
181
+ * 600000, ten minutes). An index older than this is rebuilt on session start using the parse cache,
182
+ * which is milliseconds — and rebuilding is also what restarts the file watcher, since watching
183
+ * only ever begins after a successful index.
184
+ */
185
+ autoRefreshAfterMs?: number;
177
186
  }
178
187
  export declare const Config: z<Config>;
179
188
  /**
package/lib/index.js CHANGED
@@ -27,7 +27,7 @@
27
27
  * this package ships with.
28
28
  * @module dsh-plugin-codegraph-tree-sitter
29
29
  */
30
- import { existsSync } from 'node:fs';
30
+ import { readFileSync } from 'node:fs';
31
31
  import { stat } from 'node:fs/promises';
32
32
  import { join } from 'node:path';
33
33
  import z from '@deepseek-ai/schemastery';
@@ -83,6 +83,8 @@ export const DEFAULT_CONCURRENCY = 4;
83
83
  export const DEFAULT_WATCH_DEBOUNCE_MS = 2_000;
84
84
  /** Default cap on directories watched individually, one watch each, by the fs seam. */
85
85
  export const DEFAULT_MAX_WATCHED_DIRECTORIES = 4_000;
86
+ /** Default age past which a session-open refreshes an existing index (ten minutes). */
87
+ export const DEFAULT_AUTO_REFRESH_AFTER_MS = 10 * 60 * 1000;
86
88
  export const Config = z.object({
87
89
  indexerId: z.string().default(DEFAULT_INDEXER_ID),
88
90
  languages: z.array(z.string()).default([...new Set(LANGUAGE_TABLE.map(spec => spec.language))]),
@@ -99,6 +101,7 @@ export const Config = z.object({
99
101
  watchDebounceMs: z.number().default(DEFAULT_WATCH_DEBOUNCE_MS),
100
102
  maxWatchedDirectories: z.number().default(DEFAULT_MAX_WATCHED_DIRECTORIES),
101
103
  autoIndexOnSession: z.boolean().default(true),
104
+ autoRefreshAfterMs: z.number().default(DEFAULT_AUTO_REFRESH_AFTER_MS),
102
105
  });
103
106
  /**
104
107
  * Register the tree-sitter indexer.
@@ -124,10 +127,11 @@ export function apply(ctx, config) {
124
127
  // cross-plugin relation here for an invariant to describe — just process-local bookkeeping this
125
128
  // effect's cleanup below tears down on unload.
126
129
  const watchers = new Map();
127
- // Roots this plugin instance has already started an automatic index for. The set is only a guard
128
- // against a burst of agents opened on one workspace each scheduling the same rebuild; what a
129
- // previous *process* indexed is recorded by the status sidecar `autoIndexOnSession` reads instead.
130
- const autoIndexed = new Set();
130
+ // When this instance last started an automatic index, keyed by root. This is not a "already done"
131
+ // set: an index goes stale while the process runs, so a later session in the same workspace may
132
+ // legitimately trigger another refresh. The map only keeps a burst of sessions opened at once from
133
+ // queueing the same rebuild repeatedly; the freshness decision itself reads the status sidecar.
134
+ const lastAutoRun = new Map();
131
135
  // `agent/created` is typed by `@deepseek-ai/dsh-agent`'s module augmentation. Reading the event
132
136
  // through a local structural face keeps this package's host dependencies unchanged: it needs one
133
137
  // event name and one payload shape, not the whole agent package.
@@ -192,7 +196,7 @@ export function apply(ctx, config) {
192
196
  if (resolved.autoIndexOnSession) {
193
197
  // Registered inside this effect so the listener follows the plugin's lifetime: unloading the
194
198
  // plugin must stop new sessions from scheduling work this instance can no longer run.
195
- yield agentEvents.on('agent/created', payload => autoIndexOnSession(ctx, autoIndexed, payload));
199
+ yield agentEvents.on('agent/created', payload => autoIndexOnSession(ctx, resolved, lastAutoRun, payload));
196
200
  }
197
201
  yield ctx.codegraph.registerIndexer(indexer);
198
202
  }, 'codegraph-tree-sitter');
@@ -200,30 +204,39 @@ export function apply(ctx, config) {
200
204
  /** Stable job kind of an automatic session-start index, used for retention and diagnostics. */
201
205
  const AUTO_INDEX_JOB_KIND = 'codegraph-index';
202
206
  /**
203
- * Index a new session's workspace in the background, when nothing has indexed it yet.
207
+ * Index a new session's workspace in the background — when nothing has indexed it yet, or when what
208
+ * is there has gone stale.
204
209
  *
205
210
  * Runs synchronously inside `agent/created` dispatch, so it must never block or throw: it only decides
206
211
  * whether a build is warranted and hands one to the host's job registry. A synchronous build here
207
212
  * would stall agent creation for the length of a parse, which is why a composition without background
208
213
  * jobs does nothing instead.
214
+ *
215
+ * Refreshing a stale index is what restarts the file watcher after a host restart: watching only
216
+ * begins once a successful index has established a baseline, so a workspace whose index is minutes
217
+ * old would otherwise stay frozen at whatever the last process left behind.
209
218
  * @param ctx - the plugin context, consulted for the optional job registry.
210
- * @param autoIndexed - roots this process has already started an automatic index for.
219
+ * @param config - the resolved plugin configuration, for the staleness threshold.
220
+ * @param lastAutoRun - when this instance last started an automatic index for each root.
211
221
  * @param payload - the `agent/created` dispatch payload.
212
222
  */
213
- function autoIndexOnSession(ctx, autoIndexed, payload) {
223
+ function autoIndexOnSession(ctx, config, lastAutoRun, payload) {
214
224
  try {
215
225
  const agent = payload.agent;
216
226
  const root = agent?.session?.header?.cwd;
217
227
  // No workspace means no root to claim: an agent created without a `cwd` has nothing to index.
218
228
  if (agent === undefined || !root)
219
229
  return;
220
- // Once per root per process. A second agent opened on the same workspace must not queue a second
221
- // full rebuild of a graph the first one is already building.
222
- if (autoIndexed.has(root))
230
+ const now = Date.now();
231
+ // A burst of sessions opened on one workspace must not queue the same rebuild repeatedly.
232
+ const previous = lastAutoRun.get(root);
233
+ if (previous !== undefined && now - previous < config.autoRefreshAfterMs)
223
234
  return;
224
- // An existing status sidecar means a previous run already indexed this workspace. Checked before
225
- // the job lookup so a composition without background jobs never pays for a filesystem probe.
226
- if (existsSync(join(root, INDEX_STATUS_RELATIVE_PATH)))
235
+ // A fresh sidecar means a recent run is already describing this workspace; an absent, unreadable
236
+ // or old one means the graph is missing or behind. Checked before the job lookup so a composition
237
+ // without background jobs never pays for the probe.
238
+ const indexedAt = readStatusIndexedAt(root);
239
+ if (indexedAt !== undefined && now - indexedAt < config.autoRefreshAfterMs)
227
240
  return;
228
241
  // `ctx.get` rather than an `inject` entry: `jobs` is a host composition choice, and requiring it in
229
242
  // `inject` would hide this whole indexer from a profile that runs no background jobs.
@@ -239,7 +252,7 @@ function autoIndexOnSession(ctx, autoIndexed, payload) {
239
252
  });
240
253
  // Marked only after the registry accepted the job, so a transient refusal leaves the workspace
241
254
  // eligible for the next session that opens in it.
242
- autoIndexed.add(root);
255
+ lastAutoRun.set(root, now);
243
256
  }
244
257
  catch (error) {
245
258
  // The host treats a throwing listener as vetoing publication. Losing an index is a degradation,
@@ -247,6 +260,24 @@ function autoIndexOnSession(ctx, autoIndexed, payload) {
247
260
  console.error(`codegraph-tree-sitter: automatic indexing for a new session failed: ${String(error)}`);
248
261
  }
249
262
  }
263
+ /**
264
+ * When the workspace's sidecar says its index was built.
265
+ * @param root - the workspace root.
266
+ * @returns the epoch-millisecond timestamp, or `undefined` when the sidecar is absent or unreadable —
267
+ * either way the graph cannot be trusted to be current.
268
+ */
269
+ function readStatusIndexedAt(root) {
270
+ try {
271
+ const parsed = JSON.parse(readFileSync(join(root, INDEX_STATUS_RELATIVE_PATH), 'utf8'));
272
+ if (typeof parsed !== 'object' || parsed === null)
273
+ return undefined;
274
+ const indexedAt = parsed.indexedAt;
275
+ return typeof indexedAt === 'number' && Number.isFinite(indexedAt) ? indexedAt : undefined;
276
+ }
277
+ catch {
278
+ return undefined;
279
+ }
280
+ }
250
281
  /**
251
282
  * The producer body of one automatic indexing job.
252
283
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-codegraph-tree-sitter",
3
- "version": "0.5.0",
3
+ "version": "0.5.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.5.0"
54
+ "dsh-plugin-codegraph-service": "^0.5.1"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "@deepseek-ai/cordis": ">=4.0.1",