gitnexus 1.6.8-rc.32 → 1.6.8-rc.33

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.
@@ -1,5 +1,5 @@
1
1
  import { type RegistryEntry } from '../../storage/repo-manager.js';
2
- import type { GroupConfig, RepoHandle, RepoSnapshot, StoredContract, CrossLink } from './types.js';
2
+ import type { GroupConfig, RepoHandle, RepoSnapshot, StoredContract, CrossLink, GroupManifestLink } from './types.js';
3
3
  export interface SyncOptions {
4
4
  extractorOverride?: ((repo: RepoHandle) => Promise<StoredContract[]>) | (() => Promise<StoredContract[]>);
5
5
  resolveRepoHandle?: (registryName: string, groupPath: string) => Promise<RepoHandle | null>;
@@ -18,4 +18,30 @@ export interface SyncResult {
18
18
  repoSnapshots: Record<string, RepoSnapshot>;
19
19
  }
20
20
  export declare function stableRepoPoolId(entry: RegistryEntry, allEntries: RegistryEntry[]): string;
21
+ /** A batch of manifest links whose referenced in-group repos fit one resident window. */
22
+ export interface ManifestWindow {
23
+ links: GroupManifestLink[];
24
+ /** In-group repos (group paths) this window's links reference; size ≤ maxResident. */
25
+ repos: Set<string>;
26
+ }
27
+ /**
28
+ * Partition manifest links into windows so each window references at most
29
+ * `maxResident` distinct in-group repos. Manifest resolution then materializes
30
+ * only one window's repos at a time, bounding peak pool residency regardless of
31
+ * group size (PR #2191 review, Finding 3 — windowed deferred resolution).
32
+ *
33
+ * Each link references ≤2 in-group repos, so every link fits a window when
34
+ * `maxResident ≥ 2`. Links are pre-sorted by their referenced-repo key so links
35
+ * sharing a repo land in contiguous windows — combined with release-not-close
36
+ * pooling, a hub repo stays warm across the windows that reference it (its
37
+ * lease is released, not closed, so the next window's initLbug fast-paths it).
38
+ * Every link lands in EXACTLY one window (a true partition): downstream
39
+ * dedupeCrossLinks dedupes cross-links but not contracts, so a link in two
40
+ * windows would emit duplicate contracts nothing absorbs.
41
+ *
42
+ * Repos not in `knownRepos` (dangling / unresolved) add 0 to a window's repo
43
+ * budget — the link is still placed (so it yields synthetic-UID contracts), it
44
+ * just consumes no residency.
45
+ */
46
+ export declare function partitionManifestWindows(links: GroupManifestLink[], knownRepos: Set<string>, maxResident: number): ManifestWindow[];
21
47
  export declare function syncGroup(config: GroupConfig, opts?: SyncOptions): Promise<SyncResult>;
Binary file
@@ -37,6 +37,41 @@ export { realStdoutWrite, realStderrWrite, setActiveStdoutWrite } from '../../mc
37
37
  * Call this during long-running operations to prevent the connection from being closed.
38
38
  */
39
39
  export declare const touchRepo: (repoId: string) => void;
40
+ /**
41
+ * Acquire one eviction-exemption lease on a repo (LRU + idle timeout) by
42
+ * incrementing its reference count. The repoId must match the key passed to
43
+ * initLbug (e.g. group sync leases by handle.id — the same id it inits with).
44
+ * Leasing a repoId before it enters the pool is allowed and protects the entry
45
+ * once it is created, but the lease does NOT survive a teardown: closeOne
46
+ * force-clears the count, so a later re-init of the same repoId starts
47
+ * unpinned. Each pinRepo MUST be balanced by exactly one release (the repo
48
+ * stays exempt until the last lease is released). See the pinnedRepos docstring
49
+ * for the full contract.
50
+ *
51
+ * Returns a `release` disposer (mirroring addPoolCloseListener) that releases
52
+ * THIS lease exactly once — calling it twice is a no-op, so it can never
53
+ * over-decrement a sibling holder's count. Prefer the disposer
54
+ * (`const release = pinRepo(id); try { … } finally { release(); }`) so the
55
+ * pin/release pair is leak-proof; unpinRepo remains available for callers that
56
+ * pair explicitly.
57
+ */
58
+ export declare const pinRepo: (repoId: string) => (() => void);
59
+ /**
60
+ * Release one eviction-exemption lease on a repo. The repo becomes eligible for
61
+ * automatic eviction again only once its count reaches 0 (the key is deleted).
62
+ * Idempotent at the floor: releasing a repo with no active lease is a no-op (no
63
+ * negative counts). Does NOT close the repo's pool.
64
+ */
65
+ export declare const unpinRepo: (repoId: string) => void;
66
+ /**
67
+ * Maximum number of repos a bounded multi-repo operation (e.g. group sync's
68
+ * windowed manifest resolution) should hold resident at once. Equals
69
+ * MAX_POOL_SIZE today, but exposed under an intent-named accessor so callers
70
+ * size their working set against "max repos a bounded op should hold" rather
71
+ * than coupling to the LRU eviction-cap constant, which may be tuned
72
+ * independently.
73
+ */
74
+ export declare const getMaxResidentRepos: () => number;
40
75
  /**
41
76
  * Silence stdout by replacing process.stdout.write with a no-op.
42
77
  * Uses a reference counter so nested silence/restore pairs are safe.
@@ -39,6 +39,30 @@ const MAX_POOL_SIZE = 5;
39
39
  const IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
40
40
  /** Max connections per repo (caps concurrent queries per repo) */
41
41
  const MAX_CONNS_PER_REPO = 8;
42
+ /**
43
+ * Repos exempt from AUTOMATIC eviction (LRU + idle timeout) until explicitly
44
+ * unpinned. Used by bounded multi-repo operations like `group sync`, which
45
+ * initializes one pool per repo and then resolves cross-repo manifest/workspace
46
+ * links against ALL of those pools after the init loop. Without pinning, a
47
+ * group larger than MAX_POOL_SIZE would LRU-evict the earliest repos before
48
+ * resolution runs, leaving the deferred executor closures pointing at dead pool
49
+ * entries (issue #2189).
50
+ *
51
+ * Pins are REFERENCE-COUNTED: the map holds repoId → active lease count. This
52
+ * lets overlapping holders (two windows of one sync, or two concurrent
53
+ * `group sync` calls sharing a repo) coexist safely — the repo stays exempt
54
+ * until the LAST holder releases. A boolean Set could not represent "two
55
+ * holders," so the first release would wrongly clear a pin another holder still
56
+ * needs (PR #2191 review, Finding 1).
57
+ *
58
+ * Pins block only automatic eviction (LRU + idle). Explicit teardown
59
+ * (closeOne / closeLbug) always closes the entry and force-clears its count —
60
+ * teardown is authoritative. A present key always means count ≥ 1. While every
61
+ * pooled repo is pinned, evictLRU finds no eligible victim and the pool may
62
+ * transiently exceed MAX_POOL_SIZE — the same soft-cap behavior that already
63
+ * occurs when every entry is checked out.
64
+ */
65
+ const pinnedRepos = new Map();
42
66
  // Behavior-neutral RSS tracing for the FTS evict→reload memory repro
43
67
  // (gitnexus/scripts/bench/fts-evict-reload-rss.mjs). Two invariants keep it safe
44
68
  // in the pool init/close hot path: it writes ONLY to stderr (stdout is the MCP
@@ -77,6 +101,8 @@ function ensureIdleTimer() {
77
101
  idleTimer = setInterval(() => {
78
102
  const now = Date.now();
79
103
  for (const [repoId, entry] of pool) {
104
+ if (pinnedRepos.has(repoId))
105
+ continue;
80
106
  if (now - entry.lastUsed > IDLE_TIMEOUT_MS && entry.checkedOut === 0) {
81
107
  closeOne(repoId);
82
108
  }
@@ -97,7 +123,64 @@ export const touchRepo = (repoId) => {
97
123
  }
98
124
  };
99
125
  /**
100
- * Evict the least-recently-used repo if pool is at capacity
126
+ * Acquire one eviction-exemption lease on a repo (LRU + idle timeout) by
127
+ * incrementing its reference count. The repoId must match the key passed to
128
+ * initLbug (e.g. group sync leases by handle.id — the same id it inits with).
129
+ * Leasing a repoId before it enters the pool is allowed and protects the entry
130
+ * once it is created, but the lease does NOT survive a teardown: closeOne
131
+ * force-clears the count, so a later re-init of the same repoId starts
132
+ * unpinned. Each pinRepo MUST be balanced by exactly one release (the repo
133
+ * stays exempt until the last lease is released). See the pinnedRepos docstring
134
+ * for the full contract.
135
+ *
136
+ * Returns a `release` disposer (mirroring addPoolCloseListener) that releases
137
+ * THIS lease exactly once — calling it twice is a no-op, so it can never
138
+ * over-decrement a sibling holder's count. Prefer the disposer
139
+ * (`const release = pinRepo(id); try { … } finally { release(); }`) so the
140
+ * pin/release pair is leak-proof; unpinRepo remains available for callers that
141
+ * pair explicitly.
142
+ */
143
+ export const pinRepo = (repoId) => {
144
+ pinnedRepos.set(repoId, (pinnedRepos.get(repoId) ?? 0) + 1);
145
+ let released = false;
146
+ return () => {
147
+ if (released)
148
+ return;
149
+ released = true;
150
+ unpinRepo(repoId);
151
+ };
152
+ };
153
+ /**
154
+ * Release one eviction-exemption lease on a repo. The repo becomes eligible for
155
+ * automatic eviction again only once its count reaches 0 (the key is deleted).
156
+ * Idempotent at the floor: releasing a repo with no active lease is a no-op (no
157
+ * negative counts). Does NOT close the repo's pool.
158
+ */
159
+ export const unpinRepo = (repoId) => {
160
+ const count = pinnedRepos.get(repoId);
161
+ if (count === undefined)
162
+ return;
163
+ if (count <= 1) {
164
+ pinnedRepos.delete(repoId);
165
+ }
166
+ else {
167
+ pinnedRepos.set(repoId, count - 1);
168
+ }
169
+ };
170
+ /**
171
+ * Maximum number of repos a bounded multi-repo operation (e.g. group sync's
172
+ * windowed manifest resolution) should hold resident at once. Equals
173
+ * MAX_POOL_SIZE today, but exposed under an intent-named accessor so callers
174
+ * size their working set against "max repos a bounded op should hold" rather
175
+ * than coupling to the LRU eviction-cap constant, which may be tuned
176
+ * independently.
177
+ */
178
+ export const getMaxResidentRepos = () => MAX_POOL_SIZE;
179
+ /**
180
+ * Evict the least-recently-used repo if pool is at capacity.
181
+ * Pinned repos are never chosen as the eviction victim — when every eligible
182
+ * entry is pinned, no eviction occurs and the pool transiently exceeds
183
+ * MAX_POOL_SIZE (see the pinnedRepos docstring).
101
184
  */
102
185
  function evictLRU() {
103
186
  if (pool.size < MAX_POOL_SIZE)
@@ -105,6 +188,8 @@ function evictLRU() {
105
188
  let oldestId = null;
106
189
  let oldestTime = Infinity;
107
190
  for (const [id, entry] of pool) {
191
+ if (pinnedRepos.has(id))
192
+ continue;
108
193
  if (entry.checkedOut === 0 && entry.lastUsed < oldestTime) {
109
194
  oldestTime = entry.lastUsed;
110
195
  oldestId = id;
@@ -167,6 +252,10 @@ function closeOne(repoId) {
167
252
  }
168
253
  }
169
254
  pool.delete(repoId);
255
+ // Clear any eviction pin — the entry is gone, so the pin is meaningless and
256
+ // would otherwise leak across operations in a long-lived process. Teardown
257
+ // is authoritative: an explicit close always wins over a pin.
258
+ pinnedRepos.delete(repoId);
170
259
  // Notify listeners AFTER the pool entry is gone so any cache-invalidation
171
260
  // they perform is consistent with `isLbugReady(repoId) === false`.
172
261
  for (const listener of poolCloseListeners) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.8-rc.32",
3
+ "version": "1.6.8-rc.33",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",