@zokizuan/satori-mcp 4.11.8 → 4.11.13

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/README.md CHANGED
@@ -7,8 +7,8 @@ Read-only MCP server for Satori. It gives coding agents six deterministic tools
7
7
  Use the CLI installer for normal setup:
8
8
 
9
9
  ```bash
10
- npx -y @zokizuan/satori-cli@0.4.6 install --client all
11
- npx -y @zokizuan/satori-cli@0.4.6 doctor
10
+ npx -y @zokizuan/satori-cli@latest install --client all
11
+ npx -y @zokizuan/satori-cli@latest doctor
12
12
  ```
13
13
 
14
14
  The CLI installer supports `codex`, `claude`, `opencode`, and `all`. It creates the runtime cache, writes the stable launcher, and writes client config for you. Avoid using `npx` as the resident MCP server command; first-run package resolution can exceed normal MCP startup timeouts.
@@ -16,7 +16,7 @@ The CLI installer supports `codex`, `claude`, `opencode`, and `all`. It creates
16
16
  Use `--profile default|minimal|all-text` to write repo-local `satori.toml` during install:
17
17
 
18
18
  ```bash
19
- npx -y @zokizuan/satori-cli@0.4.6 install --client all --profile minimal
19
+ npx -y @zokizuan/satori-cli@latest install --client all --profile minimal
20
20
  ```
21
21
 
22
22
  Profiles control indexing breadth, not search scope. `default` is safe-broad, `minimal` indexes source plus docs/text, and `all-text` indexes additional UTF-8 text files under the size limit. `search_codebase` still defaults to `scope=runtime`.
@@ -45,7 +45,7 @@ For Codex, add `--install-guidance-hook` only when you want an installer-managed
45
45
  Advanced direct execution is available through the package bin:
46
46
 
47
47
  ```bash
48
- npx -y @zokizuan/satori-mcp@4.11.8 --help
48
+ npx -y @zokizuan/satori-mcp@4.11.13 --help
49
49
  ```
50
50
 
51
51
  Use direct package execution for inspection, smoke tests, or unsupported harnesses. For supported clients, prefer `satori-cli install` so startup does not depend on package-manager resolution.
@@ -131,11 +131,11 @@ The full generated tool reference below is kept in the npm README for MCP client
131
131
 
132
132
  ### `manage_index`
133
133
 
134
- Manage index lifecycle operations (create/reindex/sync/status/clear) for a codebase path. Ignore-rule edits in repo-root .satoriignore/.gitignore reconcile automatically in the normal sync path. Use action="sync" for immediate convergence and action="reindex" for full rebuild recovery (preflight may block unnecessary ignore-only reindex churn unless allowUnnecessaryReindex=true). create/reindex return the kickoff response immediately and do not poll to terminal state; use action="status" to observe progress.
134
+ Manage index lifecycle operations (create/reindex/sync/status/clear/repair) for a codebase path. repair rebuilds local readiness only when existing vector payload and trusted runtime fingerprint proof match; otherwise it refuses and asks for create/reindex. Ignore-rule edits in repo-root .satoriignore/.gitignore reconcile automatically in the normal sync path. Use action="sync" for immediate convergence and action="reindex" for full rebuild recovery (preflight may block unnecessary ignore-only reindex churn unless allowUnnecessaryReindex=true). create/reindex return the kickoff response immediately and do not poll to terminal state; use action="status" to observe progress.
135
135
 
136
136
  | Parameter | Type | Required | Default | Description |
137
137
  |---|---|---|---|---|
138
- | `action` | enum("create", "reindex", "sync", "status", "clear") | yes | | Required operation to run. |
138
+ | `action` | enum("create", "reindex", "sync", "status", "clear", "repair") | yes | | Required operation to run. |
139
139
  | `path` | string | yes | | ABSOLUTE path to the target codebase. |
140
140
  | `force` | boolean | no | | Only for action='create'. Force rebuild from scratch. |
141
141
  | `allowUnnecessaryReindex` | boolean | no | | Only for action='reindex'. Override preflight block when reindex is detected as unnecessary ignore-only churn. |
package/dist/config.d.ts CHANGED
@@ -54,6 +54,7 @@ export interface CodebaseSnapshotV1 {
54
54
  }
55
55
  interface CodebaseInfoBase {
56
56
  lastUpdated: string;
57
+ collectionName?: string;
57
58
  indexFingerprint?: IndexFingerprint;
58
59
  fingerprintSource?: FingerprintSource;
59
60
  reindexReason?: 'legacy_unverified_fingerprint' | 'fingerprint_mismatch' | 'missing_fingerprint' | 'navigation_recovery_failed';
package/dist/config.js CHANGED
@@ -209,7 +209,7 @@ Environment Variables:
209
209
 
210
210
  Examples:
211
211
  # Install resident MCP config without package-manager startup on every client launch
212
- npx -y @zokizuan/satori-cli@0.4.6 install --client all
212
+ npx -y @zokizuan/satori-cli@latest install --client all
213
213
 
214
214
  # Start MCP server with OpenAI and explicit Milvus address
215
215
  OPENAI_API_KEY=sk-xxx MILVUS_ADDRESS=localhost:19530 satori
@@ -36,7 +36,7 @@ function createVectorBackendDiagnostic(code) {
36
36
  case "ZILLIZ_CLUSTER_STOPPED":
37
37
  return {
38
38
  code,
39
- message: "Vector backend is unavailable because the Zilliz Cloud cluster is stopped.",
39
+ message: "Zilliz Cloud cluster is stopped. Resume it in Zilliz Cloud, then retry the tool call.",
40
40
  hints: {
41
41
  backend: {
42
42
  code,
@@ -101,13 +101,16 @@ function createVectorBackendDiagnostic(code) {
101
101
  case "VECTOR_BACKEND_CONNECTION_CLOSED":
102
102
  return {
103
103
  code,
104
- message: "Vector backend connection closed before the tool call completed.",
104
+ message: "Vector backend connection closed before the tool call completed. Check backend health, provider environment, network connectivity, or an MCP process restart in the previous log lines.",
105
105
  hints: {
106
106
  backend: {
107
107
  code,
108
108
  provider: "unknown",
109
109
  retryable: true,
110
110
  nextSteps: [
111
+ "If the previous response mentions ZILLIZ_CLUSTER_STOPPED, resume the Zilliz cluster at https://cloud.zilliz.com.",
112
+ "If the previous response mentions MISSING_PROVIDER_CONFIG, set the missing env values in the MCP client environment and restart the client.",
113
+ "For non-Zilliz Milvus-compatible backends, confirm the service is running and reachable at MILVUS_ADDRESS.",
111
114
  "Retry the MCP tool call after confirming the backend is healthy.",
112
115
  "Restart the MCP client/session if the MCP process exited after the transport failure.",
113
116
  ],
@@ -58,6 +58,8 @@ export declare class ToolHandlers {
58
58
  private buildReindexInstruction;
59
59
  private buildReindexHint;
60
60
  private buildCreateHint;
61
+ private buildSyncHint;
62
+ private buildRepairHint;
61
63
  private buildManageIndexRecommendedAction;
62
64
  private buildStatusHint;
63
65
  private buildManageRequiresReindexHints;
@@ -83,9 +85,9 @@ export declare class ToolHandlers {
83
85
  private getContextActiveIgnorePatterns;
84
86
  private getContextIndexedExtensions;
85
87
  private getContextTrackedRelativePaths;
86
- private writeIndexCompletionMarker;
87
88
  private clearIndexCompletionMarker;
88
89
  private pruneIndexedCollectionFamily;
90
+ private pruneUnprovenStagedCollectionFamily;
89
91
  private markCodebaseCleared;
90
92
  private stringifyToolJson;
91
93
  private buildRuntimeOwnerConflictResponseIfBlocked;
@@ -96,11 +98,14 @@ export declare class ToolHandlers {
96
98
  private buildManageActionBlockedMessage;
97
99
  private getManageRetryAfterMs;
98
100
  private buildStaleLocalHint;
101
+ private getSnapshotCollectionName;
102
+ private canSyncStaleLocal;
99
103
  private buildStaleLocalMessage;
100
104
  private withProofDebugHint;
101
105
  private validateCompletionProof;
102
106
  private extractIndexedRecoveryFromCompletionProof;
103
107
  private recoverIndexedSnapshotFromCompletionProof;
108
+ private getActiveIndexedCollectionNameForSnapshotRecovery;
104
109
  private refreshSnapshotStateFromDisk;
105
110
  private isPathWithinCodebase;
106
111
  private getTrackedRootEntryForPath;
@@ -181,6 +186,13 @@ export declare class ToolHandlers {
181
186
  }>;
182
187
  isError?: boolean;
183
188
  }>;
189
+ handleRepairIndex(args: ToolArgs): Promise<{
190
+ content: Array<{
191
+ type: "text";
192
+ text: string;
193
+ }>;
194
+ isError?: boolean;
195
+ }>;
184
196
  handleSearchCode(args: ToolArgs): Promise<{
185
197
  content: {
186
198
  type: string;
@@ -151,6 +151,7 @@ export class ToolHandlers {
151
151
  buildManageIndexRecommendedAction: this.buildManageIndexRecommendedAction.bind(this),
152
152
  buildCreateHint: this.buildCreateHint.bind(this),
153
153
  buildReindexHint: this.buildReindexHint.bind(this),
154
+ buildRepairHint: this.buildRepairHint.bind(this),
154
155
  buildStatusHint: this.buildStatusHint.bind(this),
155
156
  buildStaleLocalHint: this.buildStaleLocalHint.bind(this),
156
157
  buildStaleLocalMessage: this.buildStaleLocalMessage.bind(this),
@@ -267,8 +268,10 @@ export class ToolHandlers {
267
268
  buildStaleLocalMessage: this.buildStaleLocalMessage.bind(this),
268
269
  enforceFingerprintGate: this.enforceFingerprintGate.bind(this),
269
270
  buildReindexHint: this.buildReindexHint.bind(this),
271
+ buildSyncHint: this.buildSyncHint.bind(this),
270
272
  touchWatchedCodebase: this.touchWatchedCodebase.bind(this),
271
273
  manageVectorBackendResponse: this.toolResponseBuilders.manageVectorBackendResponse.bind(this.toolResponseBuilders),
274
+ canSyncStaleLocal: this.canSyncStaleLocal.bind(this),
272
275
  };
273
276
  this.manageMaintenanceHandlers = new ManageMaintenanceHandlers(manageMaintenanceHandlersHost);
274
277
  const getManageIndexingContext = () => this.context;
@@ -299,6 +302,7 @@ export class ToolHandlers {
299
302
  getSnapshotCodebaseInfo: this.getSnapshotCodebaseInfo.bind(this),
300
303
  getSnapshotIndexedCodebases: this.getSnapshotIndexedCodebases.bind(this),
301
304
  buildManageActionBlockedMessage: this.buildManageActionBlockedMessage.bind(this),
305
+ buildCreateHint: this.buildCreateHint.bind(this),
302
306
  buildStatusHint: this.buildStatusHint.bind(this),
303
307
  getManageRetryAfterMs: this.getManageRetryAfterMs.bind(this),
304
308
  buildIndexingMetadata: this.buildIndexingMetadata.bind(this),
@@ -319,8 +323,8 @@ export class ToolHandlers {
319
323
  getContextActiveIgnorePatterns: this.getContextActiveIgnorePatterns.bind(this),
320
324
  getContextIndexedExtensions: this.getContextIndexedExtensions.bind(this),
321
325
  canonicalizeCodebasePath: this.canonicalizeCodebasePath.bind(this),
322
- writeIndexCompletionMarker: this.writeIndexCompletionMarker.bind(this),
323
326
  pruneIndexedCollectionFamily: this.pruneIndexedCollectionFamily.bind(this),
327
+ pruneUnprovenStagedCollectionFamily: this.pruneUnprovenStagedCollectionFamily.bind(this),
324
328
  getContextTrackedRelativePaths: this.getContextTrackedRelativePaths.bind(this),
325
329
  setIndexingStats: this.setIndexingStats.bind(this),
326
330
  rebuildCallGraphForIndex: this.rebuildCallGraphForIndex.bind(this),
@@ -396,6 +400,24 @@ export class ToolHandlers {
396
400
  }
397
401
  };
398
402
  }
403
+ buildSyncHint(codebasePath) {
404
+ return {
405
+ tool: "manage_index",
406
+ args: {
407
+ action: "sync",
408
+ path: codebasePath
409
+ }
410
+ };
411
+ }
412
+ buildRepairHint(codebasePath) {
413
+ return {
414
+ tool: "manage_index",
415
+ args: {
416
+ action: "repair",
417
+ path: codebasePath
418
+ }
419
+ };
420
+ }
399
421
  buildManageIndexRecommendedAction(action, codebasePath, reason) {
400
422
  return {
401
423
  tool: "manage_index",
@@ -567,9 +589,6 @@ export class ToolHandlers {
567
589
  const paths = this.contextLifecycle().getTrackedRelativePaths?.(codebasePath);
568
590
  return Array.isArray(paths) ? paths.filter((entry) => typeof entry === 'string') : [];
569
591
  }
570
- async writeIndexCompletionMarker(codebasePath, marker) {
571
- await this.contextLifecycle().writeIndexCompletionMarker?.(codebasePath, marker);
572
- }
573
592
  async clearIndexCompletionMarker(codebasePath) {
574
593
  await this.contextLifecycle().clearIndexCompletionMarker?.(codebasePath);
575
594
  }
@@ -577,6 +596,10 @@ export class ToolHandlers {
577
596
  const dropped = await this.contextLifecycle().pruneIndexedCollectionFamily?.(codebasePath, keepCollectionName);
578
597
  return Array.isArray(dropped) ? dropped.filter((entry) => typeof entry === 'string') : [];
579
598
  }
599
+ async pruneUnprovenStagedCollectionFamily(codebasePath) {
600
+ const dropped = await this.contextLifecycle().pruneUnprovenStagedCollectionFamily?.(codebasePath);
601
+ return Array.isArray(dropped) ? dropped.filter((entry) => typeof entry === 'string') : [];
602
+ }
580
603
  markCodebaseCleared(codebasePath, collectionName) {
581
604
  this.snapshotCapabilities().markCodebaseCleared?.(codebasePath, collectionName);
582
605
  }
@@ -654,7 +677,8 @@ export class ToolHandlers {
654
677
  }
655
678
  const decision = decideInterruptedIndexingRecovery(marker, this.runtimeFingerprint);
656
679
  if (decision.action === "promote_indexed") {
657
- this.snapshotManager.setCodebaseIndexed(codebasePath, decision.stats, decision.indexFingerprint, "verified");
680
+ const collectionName = await this.getActiveIndexedCollectionNameForSnapshotRecovery(codebasePath);
681
+ this.snapshotManager.setCodebaseIndexed(codebasePath, decision.stats, decision.indexFingerprint, "verified", collectionName);
658
682
  this.saveSnapshotIfSupported();
659
683
  const recoveryMode = decision.reason === "valid_marker_runtime_mismatch"
660
684
  ? " using completion marker proof from a different runtime fingerprint"
@@ -691,11 +715,48 @@ export class ToolHandlers {
691
715
  return DEFAULT_MANAGE_RETRY_AFTER_MS;
692
716
  }
693
717
  buildStaleLocalHint(codebasePath, reason) {
718
+ const preferSync = this.canSyncStaleLocal(codebasePath, reason);
719
+ const preferRepair = !preferSync && reason === "missing_marker_doc";
694
720
  return {
695
721
  completionProof: reason,
696
- recommendedAction: this.buildCreateHint(codebasePath)
722
+ recommendedAction: preferSync
723
+ ? this.buildSyncHint(codebasePath)
724
+ : preferRepair
725
+ ? this.buildRepairHint(codebasePath)
726
+ : this.buildCreateHint(codebasePath),
727
+ ...(preferSync ? { sync: this.buildSyncHint(codebasePath) } : {}),
728
+ ...(preferRepair ? { create: this.buildCreateHint(codebasePath) } : {})
697
729
  };
698
730
  }
731
+ getSnapshotCollectionName(codebasePath) {
732
+ const fromSnapshot = this.snapshotCapabilities().getCodebaseCollectionName?.(codebasePath);
733
+ if (typeof fromSnapshot === 'string' && fromSnapshot.trim().length > 0) {
734
+ return fromSnapshot.trim();
735
+ }
736
+ const fromInfo = this.getSnapshotCodebaseInfo(codebasePath)?.collectionName;
737
+ return typeof fromInfo === 'string' && fromInfo.trim().length > 0
738
+ ? fromInfo.trim()
739
+ : undefined;
740
+ }
741
+ canSyncStaleLocal(codebasePath, reason) {
742
+ if (reason !== "missing_marker_doc") {
743
+ return false;
744
+ }
745
+ const info = this.getSnapshotCodebaseInfo(codebasePath);
746
+ if (!info || (info.status !== 'indexed' && info.status !== 'sync_completed')) {
747
+ return false;
748
+ }
749
+ if (info.fingerprintSource !== 'verified' || !info.indexFingerprint) {
750
+ return false;
751
+ }
752
+ if (!this.getSnapshotCollectionName(codebasePath)) {
753
+ return false;
754
+ }
755
+ if (!this.fingerprintsEqual(info.indexFingerprint, this.runtimeFingerprint)) {
756
+ return false;
757
+ }
758
+ return true;
759
+ }
699
760
  buildStaleLocalMessage(codebasePath, requestedPath, reason) {
700
761
  const requestedPathDetail = requestedPath !== codebasePath
701
762
  ? ` Requested path: '${requestedPath}'.`
@@ -771,15 +832,26 @@ export class ToolHandlers {
771
832
  },
772
833
  };
773
834
  }
774
- recoverIndexedSnapshotFromCompletionProof(codebasePath, completionProof) {
835
+ async recoverIndexedSnapshotFromCompletionProof(codebasePath, completionProof) {
775
836
  const recovered = this.extractIndexedRecoveryFromCompletionProof(completionProof);
776
837
  if (!recovered) {
777
838
  return false;
778
839
  }
779
- this.snapshotManager.setCodebaseIndexed(codebasePath, recovered.stats, recovered.indexFingerprint, 'verified');
840
+ const collectionName = await this.getActiveIndexedCollectionNameForSnapshotRecovery(codebasePath);
841
+ this.snapshotManager.setCodebaseIndexed(codebasePath, recovered.stats, recovered.indexFingerprint, 'verified', collectionName);
780
842
  this.saveSnapshotIfSupported();
781
843
  return true;
782
844
  }
845
+ async getActiveIndexedCollectionNameForSnapshotRecovery(codebasePath) {
846
+ const context = this.context;
847
+ if (typeof context.getActiveIndexedCollectionName !== 'function') {
848
+ return undefined;
849
+ }
850
+ const collectionName = await context.getActiveIndexedCollectionName(codebasePath);
851
+ return typeof collectionName === 'string' && collectionName.trim().length > 0
852
+ ? collectionName.trim()
853
+ : undefined;
854
+ }
783
855
  refreshSnapshotStateFromDisk() {
784
856
  const snapshotManager = this.snapshotManager;
785
857
  if (typeof snapshotManager.refreshFromDiskIfChanged !== 'function') {
@@ -818,7 +890,11 @@ export class ToolHandlers {
818
890
  }
819
891
  let collectionName;
820
892
  try {
821
- if (typeof context.getActiveIndexedCollectionName === 'function') {
893
+ const snapshotCollectionName = this.getSnapshotCollectionName(codebasePath);
894
+ if (snapshotCollectionName) {
895
+ collectionName = snapshotCollectionName;
896
+ }
897
+ else if (typeof context.getActiveIndexedCollectionName === 'function') {
822
898
  const activeCollectionName = await context.getActiveIndexedCollectionName(codebasePath);
823
899
  if (typeof activeCollectionName === 'string' && activeCollectionName.trim().length > 0) {
824
900
  collectionName = activeCollectionName;
@@ -1179,6 +1255,7 @@ export class ToolHandlers {
1179
1255
  }
1180
1256
  buildRequiresReindexFileOutlinePayload(codebasePath, input, detail, reason = 'requires_reindex') {
1181
1257
  const detailLine = detail ? `${detail}\n\n` : '';
1258
+ const preferRepair = reason === 'missing_symbol_registry' || reason === 'missing_relationship_sidecar';
1182
1259
  return {
1183
1260
  status: 'requires_reindex',
1184
1261
  reason,
@@ -1186,8 +1263,11 @@ export class ToolHandlers {
1186
1263
  file: input.file,
1187
1264
  outline: null,
1188
1265
  hasMore: false,
1189
- message: `${detailLine}Relationship-backed navigation sidecars are missing or incompatible. Please run manage_index with {"action":"reindex","path":"${codebasePath}"}.`,
1266
+ message: preferRepair
1267
+ ? `${detailLine}Relationship-backed navigation sidecars are missing. Please run manage_index with {"action":"repair","path":"${codebasePath}"}.`
1268
+ : `${detailLine}Relationship-backed navigation sidecars are missing or incompatible. Please run manage_index with {"action":"reindex","path":"${codebasePath}"}.`,
1190
1269
  hints: {
1270
+ ...(preferRepair ? { repair: this.buildRepairHint(codebasePath) } : {}),
1191
1271
  reindex: this.buildReindexHint(codebasePath)
1192
1272
  }
1193
1273
  };
@@ -1304,6 +1384,9 @@ export class ToolHandlers {
1304
1384
  async handleReindexCodebase(args) {
1305
1385
  return this.manageIndexingHandlers.handleReindexCodebase(args);
1306
1386
  }
1387
+ async handleRepairIndex(args) {
1388
+ return this.manageIndexingHandlers.handleRepairIndex(args);
1389
+ }
1307
1390
  async handleSearchCode(args) {
1308
1391
  const scope = (typeof args.scope === 'string' ? args.scope : 'runtime');
1309
1392
  const resultMode = (typeof args.resultMode === 'string' ? args.resultMode : 'grouped');
@@ -1382,8 +1465,11 @@ export class ToolHandlers {
1382
1465
  buildFreshnessBlockedSearchPayload: (codebasePath, freshnessDecision, searchContext) => this.buildFreshnessBlockedSearchPayload(codebasePath, freshnessDecision, searchContext),
1383
1466
  buildManageIndexRecommendedAction: (action, codebasePath, rationale) => this.buildManageIndexRecommendedAction(action, codebasePath, rationale),
1384
1467
  buildCreateHint: (codebasePath) => this.buildCreateHint(codebasePath),
1468
+ buildSyncHint: (codebasePath) => this.buildSyncHint(codebasePath),
1469
+ buildRepairHint: (codebasePath) => this.buildRepairHint(codebasePath),
1385
1470
  buildStaleLocalHint: (codebasePath, reason) => this.buildStaleLocalHint(codebasePath, reason),
1386
1471
  buildStaleLocalMessage: (codebasePath, requestedPath, reason) => this.buildStaleLocalMessage(codebasePath, requestedPath, reason),
1472
+ canSyncStaleLocal: (codebasePath, reason) => this.canSyncStaleLocal(codebasePath, reason),
1387
1473
  withProofDebugHint: (payload, proofDebugHint) => this.withProofDebugHint(payload, proofDebugHint),
1388
1474
  isPartialIndexNavigationUnavailable: (info) => this.isPartialIndexNavigationUnavailable(info),
1389
1475
  partialIndexWarnings: [
@@ -1,4 +1,4 @@
1
- import { Context, IndexCompletionMarkerDocument } from "@zokizuan/satori-core";
1
+ import { Context } from "@zokizuan/satori-core";
2
2
  import type { SnapshotManager } from "./snapshot.js";
3
3
  import type { SyncManager } from "./sync.js";
4
4
  import { ManageIndexAction } from "./manage-types.js";
@@ -44,14 +44,15 @@ type ManageIndexingHandlersHost = {
44
44
  getSnapshotIndexingCodebases(): string[];
45
45
  getSnapshotCodebaseInfo(codebasePath: string): Record<string, unknown> | undefined;
46
46
  getSnapshotIndexedCodebases(): string[];
47
- buildManageActionBlockedMessage(codebasePath: string, action: "create" | "reindex"): string;
47
+ buildManageActionBlockedMessage(codebasePath: string, action: "create" | "reindex" | "repair"): string;
48
+ buildCreateHint(codebasePath: string): Record<string, unknown>;
48
49
  buildStatusHint(codebasePath: string): Record<string, unknown>;
49
50
  getManageRetryAfterMs(): number;
50
51
  buildIndexingMetadata(codebasePath: string): Record<string, unknown> | undefined;
51
52
  buildReindexInstruction(codebasePath: string, detail?: string): string;
52
53
  buildManageRequiresReindexHints(codebasePath: string): Record<string, unknown>;
53
54
  validateCompletionProof(codebasePath: string): Promise<CompletionProofValidationResult>;
54
- recoverIndexedSnapshotFromCompletionProof(codebasePath: string, proof: CompletionProofValidationResult): boolean;
55
+ recoverIndexedSnapshotFromCompletionProof(codebasePath: string, proof: CompletionProofValidationResult): Promise<boolean>;
55
56
  isZillizBackend(): boolean;
56
57
  resolveCollectionName(codebasePath: string): string;
57
58
  dropZillizCollectionForCreate(collectionName: string): Promise<{
@@ -67,8 +68,8 @@ type ManageIndexingHandlersHost = {
67
68
  getContextActiveIgnorePatterns(codebasePath: string): string[];
68
69
  getContextIndexedExtensions(codebasePath: string): string[];
69
70
  canonicalizeCodebasePath(codebasePath: string): string;
70
- writeIndexCompletionMarker(codebasePath: string, marker: IndexCompletionMarkerDocument): Promise<void>;
71
71
  pruneIndexedCollectionFamily(codebasePath: string, keepCollectionName: string): Promise<string[]>;
72
+ pruneUnprovenStagedCollectionFamily(codebasePath: string): Promise<string[]>;
72
73
  getContextTrackedRelativePaths(codebasePath: string): string[];
73
74
  setIndexingStats(stats: {
74
75
  indexedFiles: number;
@@ -82,8 +83,11 @@ type ManageIndexingHandlersHost = {
82
83
  export declare class ManageIndexingHandlers {
83
84
  private readonly host;
84
85
  constructor(host: ManageIndexingHandlersHost);
86
+ private isStagedCollectionName;
87
+ private cleanupFailedStagedCollection;
85
88
  handleIndexCodebase(args: IndexCodebaseArgs): Promise<ToolTextResponse>;
86
89
  handleReindexCodebase(args: ReindexCodebaseArgs): Promise<ToolTextResponse>;
90
+ handleRepairIndex(args: Record<string, unknown>): Promise<ToolTextResponse>;
87
91
  private startBackgroundIndexing;
88
92
  }
89
93
  export {};
@@ -1,8 +1,24 @@
1
1
  import * as fs from "fs";
2
2
  import * as crypto from "node:crypto";
3
- import { COLLECTION_LIMIT_MESSAGE, RemoteCollectionDeletePendingError, } from "@zokizuan/satori-core";
3
+ import { COLLECTION_LIMIT_MESSAGE, deleteCollectionWithVerification, RemoteCollectionDeletePendingError, } from "@zokizuan/satori-core";
4
4
  import { classifyVectorBackendError, } from "./backend-diagnostics.js";
5
5
  import { ensureAbsolutePath, trackCodebasePath } from "../utils.js";
6
+ function isMatchingVerifiedFingerprint(info, runtimeFingerprint) {
7
+ if (info?.fingerprintSource !== "verified") {
8
+ return undefined;
9
+ }
10
+ const fingerprint = info.indexFingerprint;
11
+ if (!fingerprint || typeof fingerprint !== "object") {
12
+ return undefined;
13
+ }
14
+ const record = fingerprint;
15
+ const matches = record.embeddingProvider === runtimeFingerprint.embeddingProvider
16
+ && record.embeddingModel === runtimeFingerprint.embeddingModel
17
+ && Number(record.embeddingDimension) === Number(runtimeFingerprint.embeddingDimension)
18
+ && record.vectorStoreProvider === runtimeFingerprint.vectorStoreProvider
19
+ && record.schemaVersion === runtimeFingerprint.schemaVersion;
20
+ return matches ? fingerprint : undefined;
21
+ }
6
22
  const COLLECTION_LIMIT_PATTERNS = [
7
23
  /exceeded the limit number of collections/i,
8
24
  /collection limit/i,
@@ -96,6 +112,21 @@ export class ManageIndexingHandlers {
96
112
  constructor(host) {
97
113
  this.host = host;
98
114
  }
115
+ isStagedCollectionName(collectionName) {
116
+ return typeof collectionName === "string" && collectionName.includes("__gen_");
117
+ }
118
+ async cleanupFailedStagedCollection(codebasePath, collectionName) {
119
+ if (!this.isStagedCollectionName(collectionName)) {
120
+ return;
121
+ }
122
+ try {
123
+ await deleteCollectionWithVerification(this.host.context.getVectorStore(), collectionName);
124
+ console.log(`[BACKGROUND-INDEX] Cleaned failed staged collection '${collectionName}' for '${codebasePath}'.`);
125
+ }
126
+ catch (cleanupError) {
127
+ console.warn(`[BACKGROUND-INDEX] Failed to clean staged collection '${collectionName}' after indexing failure for '${codebasePath}': ${formatUnknownError(cleanupError)}`);
128
+ }
129
+ }
99
130
  async handleIndexCodebase(args) {
100
131
  const { path: codebasePath, force, customExtensions, ignorePatterns, zillizDropCollection } = args;
101
132
  const forceReindex = force || false;
@@ -149,7 +180,7 @@ export class ManageIndexingHandlers {
149
180
  const isIndexedInSnapshot = this.host.getSnapshotIndexedCodebases().includes(absolutePath);
150
181
  if (!forceReindex && !isIndexedInSnapshot) {
151
182
  const proof = await this.host.validateCompletionProof(absolutePath);
152
- if (this.host.recoverIndexedSnapshotFromCompletionProof(absolutePath, proof)) {
183
+ if (await this.host.recoverIndexedSnapshotFromCompletionProof(absolutePath, proof)) {
153
184
  if (proof.outcome === "fingerprint_mismatch") {
154
185
  return this.host.manageResponse(manageAction, absolutePath, "requires_reindex", this.host.buildReindexInstruction(absolutePath, "Recovered local readiness from completion marker proof, but the current runtime fingerprint does not match the existing index."), {
155
186
  ...preflightOptions,
@@ -200,6 +231,10 @@ export class ManageIndexingHandlers {
200
231
  }
201
232
  const stagedCollectionName = this.host.resolveStagedCollectionName(absolutePath, `run_${crypto.randomUUID()}`);
202
233
  try {
234
+ const prunedStagedCollections = await this.host.pruneUnprovenStagedCollectionFamily(absolutePath);
235
+ if (prunedStagedCollections.length > 0) {
236
+ console.log(`[INDEX-VALIDATION] 🧹 Removed ${prunedStagedCollections.length} unproven staged collection(s): ${prunedStagedCollections.join(", ")}`);
237
+ }
203
238
  console.log("[INDEX-VALIDATION] 🔍 Validating collection creation capability");
204
239
  const canCreateCollection = await this.host.context.getVectorStore().checkCollectionLimit();
205
240
  if (!canCreateCollection) {
@@ -332,12 +367,89 @@ export class ManageIndexingHandlers {
332
367
  __reindexPreflight: forwardedPreflight,
333
368
  });
334
369
  }
370
+ async handleRepairIndex(args) {
371
+ const codebasePath = args.path;
372
+ if (typeof codebasePath !== "string" || codebasePath.trim().length === 0) {
373
+ return this.host.manageResponse("repair", "", "error", "Error: Path is required.");
374
+ }
375
+ let absolutePath = codebasePath;
376
+ try {
377
+ absolutePath = ensureAbsolutePath(codebasePath);
378
+ if (!fs.existsSync(absolutePath)) {
379
+ return this.host.manageResponse("repair", absolutePath, "error", `Error: Path '${absolutePath}' does not exist. Original input: '${codebasePath}'`);
380
+ }
381
+ const stat = fs.statSync(absolutePath);
382
+ if (!stat.isDirectory()) {
383
+ return this.host.manageResponse("repair", absolutePath, "error", `Error: Path '${absolutePath}' is not a directory`);
384
+ }
385
+ const runtimeOwnerConflict = await this.host.buildRuntimeOwnerConflictResponseIfBlocked("repair", absolutePath);
386
+ if (runtimeOwnerConflict) {
387
+ return runtimeOwnerConflict;
388
+ }
389
+ if (this.host.getSnapshotIndexingCodebases().includes(absolutePath)) {
390
+ return this.host.manageResponse("repair", absolutePath, "not_ready", this.host.buildManageActionBlockedMessage(absolutePath, "repair"), {
391
+ reason: "indexing",
392
+ hints: {
393
+ status: this.host.buildStatusHint(absolutePath),
394
+ retryAfterMs: this.host.getManageRetryAfterMs(),
395
+ indexing: this.host.buildIndexingMetadata(absolutePath),
396
+ },
397
+ });
398
+ }
399
+ const trustedFingerprint = isMatchingVerifiedFingerprint(this.host.getSnapshotCodebaseInfo(absolutePath), this.host.runtimeFingerprint);
400
+ const result = await this.host.context.repairIndex(absolutePath, { trustedFingerprint });
401
+ if (result.status === "ok") {
402
+ await this.host.rebuildCallGraphForIndex(absolutePath);
403
+ await this.host.touchWatchedCodebase(absolutePath);
404
+ const stats = {
405
+ indexedFiles: result.indexedFiles || 0,
406
+ totalChunks: result.totalChunks || 0,
407
+ status: "completed"
408
+ };
409
+ this.host.snapshotManager.setCodebaseIndexed(absolutePath, stats, this.host.runtimeFingerprint, "verified", result.collectionName);
410
+ const trackedRelativePaths = result.trackedRelativePaths;
411
+ this.host.snapshotManager.setCodebaseIndexManifest(absolutePath, Array.isArray(trackedRelativePaths)
412
+ ? trackedRelativePaths.filter((entry) => typeof entry === "string")
413
+ : this.host.getContextTrackedRelativePaths(absolutePath));
414
+ this.host.setIndexingStats(stats);
415
+ this.host.saveSnapshotIfSupported();
416
+ const warningsArray = Array.isArray(result.warnings) ? result.warnings : [];
417
+ return this.host.manageResponse("repair", absolutePath, "ok", result.message, {
418
+ warnings: warningsArray,
419
+ });
420
+ }
421
+ else if (result.status === "requires_reindex") {
422
+ return this.host.manageResponse("repair", absolutePath, "requires_reindex", result.message, {
423
+ reason: "requires_reindex",
424
+ hints: this.host.buildManageRequiresReindexHints(absolutePath),
425
+ });
426
+ }
427
+ else {
428
+ return this.host.manageResponse("repair", absolutePath, "blocked", result.message, {
429
+ reason: result.reason || "needs_create",
430
+ hints: {
431
+ create: this.host.buildCreateHint(absolutePath),
432
+ missingCount: result.missingCount,
433
+ }
434
+ });
435
+ }
436
+ }
437
+ catch (error) {
438
+ console.error("Error in handleRepairIndex:", error);
439
+ const vectorBackendDiagnostic = classifyVectorBackendError(error);
440
+ if (vectorBackendDiagnostic) {
441
+ return this.host.manageVectorBackendResponse("repair", absolutePath, vectorBackendDiagnostic);
442
+ }
443
+ return this.host.manageResponse("repair", absolutePath, "error", `Error performing repair: ${formatUnknownError(error)}`);
444
+ }
445
+ }
335
446
  async startBackgroundIndexing(codebasePath, forceReindex, writeCollectionName) {
336
447
  const absolutePath = codebasePath;
337
448
  let lastSaveTime = 0;
449
+ let targetCollectionName;
338
450
  try {
339
451
  console.log(`[BACKGROUND-INDEX] Starting background indexing for: ${absolutePath}`);
340
- const targetCollectionName = typeof writeCollectionName === "string" && writeCollectionName.trim().length > 0
452
+ targetCollectionName = typeof writeCollectionName === "string" && writeCollectionName.trim().length > 0
341
453
  ? writeCollectionName
342
454
  : this.host.resolveCollectionName(absolutePath);
343
455
  this.host.setWriteCollectionOverride(absolutePath, targetCollectionName);
@@ -370,15 +482,6 @@ export class ManageIndexingHandlers {
370
482
  console.log(`[BACKGROUND-INDEX] Progress: ${progress.phase} - ${progress.percentage}% (${progress.current}/${progress.total})`);
371
483
  });
372
484
  console.log(`[BACKGROUND-INDEX] ✅ Indexing completed successfully! Files: ${stats.indexedFiles}, Chunks: ${stats.totalChunks}`);
373
- await this.host.writeIndexCompletionMarker(absolutePath, {
374
- kind: "satori_index_completion_v1",
375
- codebasePath: this.host.canonicalizeCodebasePath(absolutePath),
376
- fingerprint: this.host.runtimeFingerprint,
377
- indexedFiles: stats.indexedFiles,
378
- totalChunks: stats.totalChunks,
379
- completedAt: new Date().toISOString(),
380
- runId: `run_${crypto.randomUUID()}`,
381
- });
382
485
  try {
383
486
  const droppedCollections = await this.host.pruneIndexedCollectionFamily(absolutePath, targetCollectionName);
384
487
  if (droppedCollections.length > 0) {
@@ -388,7 +491,7 @@ export class ManageIndexingHandlers {
388
491
  catch (pruneError) {
389
492
  console.warn(`[BACKGROUND-INDEX] Failed to retire superseded generations for '${absolutePath}': ${formatUnknownError(pruneError)}`);
390
493
  }
391
- this.host.snapshotManager.setCodebaseIndexed(absolutePath, stats, this.host.runtimeFingerprint, "verified");
494
+ this.host.snapshotManager.setCodebaseIndexed(absolutePath, stats, this.host.runtimeFingerprint, "verified", targetCollectionName);
392
495
  this.host.snapshotManager.setCodebaseIndexManifest(absolutePath, this.host.getContextTrackedRelativePaths(absolutePath));
393
496
  this.host.setIndexingStats({ indexedFiles: stats.indexedFiles, totalChunks: stats.totalChunks });
394
497
  await this.host.syncManager.recordCurrentIgnoreControlSignature(absolutePath);
@@ -413,6 +516,7 @@ export class ManageIndexingHandlers {
413
516
  catch (clearError) {
414
517
  console.warn(`[BACKGROUND-INDEX] Failed to clear completion marker after indexing error for '${absolutePath}': ${formatUnknownError(clearError)}`);
415
518
  }
519
+ await this.cleanupFailedStagedCollection(absolutePath, targetCollectionName);
416
520
  this.host.snapshotManager.setCodebaseIndexFailed(absolutePath, errorMessage, this.host.getSnapshotIndexingProgress(absolutePath));
417
521
  this.host.saveSnapshotIfSupported();
418
522
  console.error(`[BACKGROUND-INDEX] Indexing failed for ${absolutePath}: ${errorMessage}`);
@@ -40,8 +40,10 @@ type ManageMaintenanceHandlersHost = {
40
40
  buildReindexInstruction(codebasePath: string, detail?: string): string;
41
41
  buildCompatibilityStatusLines(codebasePath: string): string;
42
42
  buildManageRequiresReindexHints(codebasePath: string): Record<string, unknown>;
43
+ buildSyncHint(codebasePath: string): Record<string, unknown>;
43
44
  buildStaleLocalHint(codebasePath: string, reason: string): Record<string, unknown>;
44
45
  buildStaleLocalMessage(codebasePath: string, requestedPath: string, reason: string): string;
46
+ canSyncStaleLocal(codebasePath: string, reason: string): boolean;
45
47
  enforceFingerprintGate(codebasePath: string): {
46
48
  blockedResponse?: ToolTextResponse;
47
49
  message?: string;
@@ -208,11 +208,15 @@ export class ManageMaintenanceHandlers {
208
208
  envelopePath = trackedRootState.codebasePath;
209
209
  envelopeStatus = "not_indexed";
210
210
  envelopeReason = "not_indexed";
211
+ const syncable = this.host.canSyncStaleLocal(trackedRootState.codebasePath, trackedRootState.reason);
211
212
  envelopeHints = {
213
+ ...(syncable ? { sync: this.host.buildSyncHint(trackedRootState.codebasePath) } : {}),
212
214
  create: this.host.buildCreateHint(trackedRootState.codebasePath),
213
215
  staleLocal: this.host.buildStaleLocalHint(trackedRootState.codebasePath, trackedRootState.reason),
214
216
  };
215
- statusMessage = `❌ ${this.host.buildStaleLocalMessage(trackedRootState.codebasePath, absolutePath, trackedRootState.reason)} Run manage_index with {"action":"create","path":"${trackedRootState.codebasePath}"} to repair it.`;
217
+ const nextAction = syncable ? "sync" : trackedRootState.reason === "missing_marker_doc" ? "repair" : "create";
218
+ const nextVerb = syncable ? "sync it" : nextAction === "repair" ? "repair it" : "create it";
219
+ statusMessage = `❌ ${this.host.buildStaleLocalMessage(trackedRootState.codebasePath, absolutePath, trackedRootState.reason)} Run manage_index with {"action":"${nextAction}","path":"${trackedRootState.codebasePath}"} to ${nextVerb}.`;
216
220
  }
217
221
  else if (trackedRootState.state === "missing_collection") {
218
222
  envelopePath = trackedRootState.codebasePath;
@@ -1,7 +1,7 @@
1
1
  import { WarningCode } from "./warnings.js";
2
- export type ManageIndexAction = "create" | "reindex" | "sync" | "status" | "clear";
2
+ export type ManageIndexAction = "create" | "reindex" | "sync" | "status" | "clear" | "repair";
3
3
  export type ManageIndexStatus = "ok" | "not_ready" | "not_indexed" | "requires_reindex" | "blocked" | "error";
4
- export type ManageIndexReason = "indexing" | "not_indexed" | "requires_reindex" | "unnecessary_reindex_ignore_only" | "preflight_unknown" | "backend_timeout" | "remote_delete_pending" | "missing_provider_config" | "vector_backend_unavailable" | "runtime_owner_conflict";
4
+ export type ManageIndexReason = "indexing" | "not_indexed" | "requires_reindex" | "unnecessary_reindex_ignore_only" | "preflight_unknown" | "backend_timeout" | "remote_delete_pending" | "missing_provider_config" | "vector_backend_unavailable" | "runtime_owner_conflict" | "needs_create";
5
5
  export type VectorBackendResponseCode = "ZILLIZ_CLUSTER_STOPPED" | "VECTOR_BACKEND_AUTH_FAILED" | "VECTOR_BACKEND_UNREACHABLE" | "VECTOR_BACKEND_TIMEOUT" | "VECTOR_BACKEND_CONNECTION_CLOSED";
6
6
  export type ManageReindexPreflightOutcome = "reindex_required" | "reindex_unnecessary_ignore_only" | "unknown" | "probe_failed";
7
7
  export interface ManageIndexToolHint {
@@ -31,7 +31,7 @@ export type SearchFrontDoorHost = {
31
31
  buildRequiresReindexPayload: (codebasePath: string, detail: string | undefined, searchContext: SearchFrontDoorSearchContext) => SearchResponseEnvelope;
32
32
  buildNotReadySearchPayload: (codebasePath: string, searchContext: SearchFrontDoorSearchContext) => SearchResponseEnvelope;
33
33
  buildFreshnessBlockedSearchPayload: (codebasePath: string, freshnessDecision: FreshnessDecision, searchContext: SearchFrontDoorSearchContext) => SearchResponseEnvelope | null;
34
- buildManageIndexRecommendedAction: (action: "create" | "reindex" | "sync" | "status", codebasePath: string, rationale: string) => SearchRecommendedNextAction;
34
+ buildManageIndexRecommendedAction: (action: "create" | "reindex" | "sync" | "status" | "repair", codebasePath: string, rationale: string) => SearchRecommendedNextAction;
35
35
  buildCreateHint: (codebasePath: string) => {
36
36
  tool: string;
37
37
  args: {
@@ -39,8 +39,23 @@ export type SearchFrontDoorHost = {
39
39
  path: string;
40
40
  };
41
41
  };
42
+ buildSyncHint: (codebasePath: string) => {
43
+ tool: string;
44
+ args: {
45
+ action: string;
46
+ path: string;
47
+ };
48
+ };
49
+ buildRepairHint: (codebasePath: string) => {
50
+ tool: string;
51
+ args: {
52
+ action: string;
53
+ path: string;
54
+ };
55
+ };
42
56
  buildStaleLocalHint: (codebasePath: string, reason: CompletionProofReason) => Record<string, unknown>;
43
57
  buildStaleLocalMessage: (codebasePath: string, requestedPath: string, reason: CompletionProofReason) => string;
58
+ canSyncStaleLocal: (codebasePath: string, reason: CompletionProofReason) => boolean;
44
59
  withProofDebugHint: <T extends object>(payload: T, proofDebugHint?: CompletionProbeDebugHint) => T;
45
60
  isPartialIndexNavigationUnavailable: (info: unknown) => boolean;
46
61
  partialIndexWarnings: readonly string[];
@@ -48,6 +48,9 @@ function buildBlockedReadinessPayload(state, searchContext, host) {
48
48
  };
49
49
  }
50
50
  if (state.state === "stale_local") {
51
+ const preferSync = host.canSyncStaleLocal(state.codebasePath, state.reason);
52
+ const preferRepair = !preferSync && state.reason === "missing_marker_doc";
53
+ const action = preferSync ? "sync" : preferRepair ? "repair" : "create";
51
54
  return {
52
55
  status: "not_indexed",
53
56
  reason: "not_indexed",
@@ -59,8 +62,14 @@ function buildBlockedReadinessPayload(state, searchContext, host) {
59
62
  resultMode: searchContext.resultMode,
60
63
  freshnessDecision: null,
61
64
  message: host.buildStaleLocalMessage(state.codebasePath, searchContext.path, state.reason),
62
- recommendedNextAction: host.buildManageIndexRecommendedAction("create", state.codebasePath, "Create a fresh index because local readiness metadata is stale."),
65
+ recommendedNextAction: host.buildManageIndexRecommendedAction(action, state.codebasePath, preferSync
66
+ ? "Run incremental sync; the local snapshot proves the committed collection and can restore the missing completion marker."
67
+ : preferRepair
68
+ ? "Repair local readiness because completion marker proof is missing and no syncable snapshot collection is available."
69
+ : "Create a fresh index because local readiness metadata is stale."),
63
70
  hints: {
71
+ ...(preferSync ? { sync: host.buildSyncHint(state.codebasePath) } : {}),
72
+ ...(preferRepair ? { repair: host.buildRepairHint(state.codebasePath) } : {}),
64
73
  create: host.buildCreateHint(state.codebasePath),
65
74
  staleLocal: host.buildStaleLocalHint(state.codebasePath, state.reason),
66
75
  },
@@ -89,20 +98,24 @@ export async function runSearchFrontDoor(input, host) {
89
98
  }
90
99
  trackCodebasePath(absolutePath);
91
100
  const trackedRootState = await host.prepareInitialTrackedRootRead(absolutePath);
92
- const blockedReadinessPayload = buildBlockedReadinessPayload(trackedRootState, searchContext, host);
93
- if (blockedReadinessPayload) {
94
- return {
95
- kind: "blocked",
96
- payload: blockedReadinessPayload,
97
- };
101
+ const canSyncInitialStaleLocal = trackedRootState.state === "stale_local"
102
+ && host.canSyncStaleLocal(trackedRootState.codebasePath, trackedRootState.reason);
103
+ if (!canSyncInitialStaleLocal) {
104
+ const blockedReadinessPayload = buildBlockedReadinessPayload(trackedRootState, searchContext, host);
105
+ if (blockedReadinessPayload) {
106
+ return {
107
+ kind: "blocked",
108
+ payload: blockedReadinessPayload,
109
+ };
110
+ }
98
111
  }
99
- if (trackedRootState.state !== "ready") {
112
+ if (trackedRootState.state !== "ready" && !canSyncInitialStaleLocal) {
100
113
  throw new Error(`Unexpected non-ready tracked root state after readiness gating: ${trackedRootState.state}`);
101
114
  }
102
- let searchableRoot = trackedRootState.root;
103
- let effectiveRoot = searchableRoot.path || absolutePath;
104
- let proofDebugHint = trackedRootState.proofDebugHint;
105
- let partialIndexSearchWarnings = buildPartialIndexWarnings(host, searchableRoot);
115
+ let searchableRoot = trackedRootState.state === "ready" ? trackedRootState.root : null;
116
+ let effectiveRoot = searchableRoot?.path || (trackedRootState.state === "stale_local" ? trackedRootState.codebasePath : absolutePath);
117
+ let proofDebugHint = trackedRootState.state === "ready" ? trackedRootState.proofDebugHint : undefined;
118
+ let partialIndexSearchWarnings = searchableRoot ? buildPartialIndexWarnings(host, searchableRoot) : [];
106
119
  const freshnessDecision = await host.ensureSearchFreshness(effectiveRoot);
107
120
  host.noteFreshnessMode(freshnessDecision.mode);
108
121
  const freshnessBlockedPayload = host.buildFreshnessBlockedSearchPayload(effectiveRoot, freshnessDecision, searchContext);
@@ -83,6 +83,7 @@ export declare class SnapshotManager {
83
83
  getIndexingProgress(codebasePath: string): number | undefined;
84
84
  getCodebaseStatus(codebasePath: string): CodebaseInfo['status'] | 'not_found';
85
85
  getCodebaseInfo(codebasePath: string): CodebaseInfo | undefined;
86
+ getCodebaseCollectionName(codebasePath: string): string | undefined;
86
87
  getAllCodebases(): Array<{
87
88
  path: string;
88
89
  info: CodebaseInfo;
@@ -97,13 +98,13 @@ export declare class SnapshotManager {
97
98
  indexedFiles: number;
98
99
  totalChunks: number;
99
100
  status: 'completed' | 'limit_reached';
100
- }, indexFingerprint?: IndexFingerprint, fingerprintSource?: FingerprintSource): void;
101
+ }, indexFingerprint?: IndexFingerprint, fingerprintSource?: FingerprintSource, collectionName?: string): void;
101
102
  setCodebaseIndexFailed(codebasePath: string, errorMessage: string, lastAttemptedPercentage?: number): void;
102
103
  setCodebaseSyncCompleted(codebasePath: string, stats: {
103
104
  added: number;
104
105
  removed: number;
105
106
  modified: number;
106
- }, indexFingerprint?: IndexFingerprint, fingerprintSource?: FingerprintSource): void;
107
+ }, indexFingerprint?: IndexFingerprint, fingerprintSource?: FingerprintSource, collectionName?: string): void;
107
108
  setCodebaseRequiresReindex(codebasePath: string, reason: AccessGateReason, message?: string): void;
108
109
  private recoverCodebaseFromResolvedFingerprintMismatch;
109
110
  setCodebaseCallGraphSidecar(codebasePath: string, sidecar: CallGraphSidecarInfo): void;
@@ -384,6 +384,9 @@ export class SnapshotManager {
384
384
  if (rawInfo.indexFingerprint !== undefined && !this.isValidIndexFingerprint(rawInfo.indexFingerprint)) {
385
385
  return false;
386
386
  }
387
+ if (rawInfo.collectionName !== undefined && typeof rawInfo.collectionName !== "string") {
388
+ return false;
389
+ }
387
390
  if (rawInfo.fingerprintSource !== undefined && rawInfo.fingerprintSource !== "verified" && rawInfo.fingerprintSource !== "assumed_v2") {
388
391
  return false;
389
392
  }
@@ -802,6 +805,12 @@ export class SnapshotManager {
802
805
  getCodebaseInfo(codebasePath) {
803
806
  return this.codebaseInfoMap.get(codebasePath);
804
807
  }
808
+ getCodebaseCollectionName(codebasePath) {
809
+ const collectionName = this.codebaseInfoMap.get(codebasePath)?.collectionName;
810
+ return typeof collectionName === "string" && collectionName.trim().length > 0
811
+ ? collectionName.trim()
812
+ : undefined;
813
+ }
805
814
  getAllCodebases() {
806
815
  return Array.from(this.codebaseInfoMap.entries()).map(([p, info]) => ({ path: p, info }));
807
816
  }
@@ -849,6 +858,7 @@ export class SnapshotManager {
849
858
  indexFingerprint: existing?.indexFingerprint,
850
859
  fingerprintSource: existing?.fingerprintSource,
851
860
  reindexReason: existing?.reindexReason,
861
+ collectionName: existing?.collectionName,
852
862
  callGraphSidecar: existing?.callGraphSidecar,
853
863
  indexManifest: existing?.indexManifest,
854
864
  ignoreRulesVersion: existing?.ignoreRulesVersion,
@@ -858,15 +868,19 @@ export class SnapshotManager {
858
868
  this.markDirty();
859
869
  this.refreshDerivedState();
860
870
  }
861
- setCodebaseIndexed(codebasePath, stats, indexFingerprint, fingerprintSource = 'verified') {
871
+ setCodebaseIndexed(codebasePath, stats, indexFingerprint, fingerprintSource = 'verified', collectionName) {
862
872
  this.markCodebasePresent(codebasePath);
863
873
  const existing = this.codebaseInfoMap.get(codebasePath);
874
+ const resolvedCollectionName = typeof collectionName === "string" && collectionName.trim().length > 0
875
+ ? collectionName.trim()
876
+ : existing?.collectionName;
864
877
  const info = {
865
878
  status: 'indexed',
866
879
  indexedFiles: stats.indexedFiles,
867
880
  totalChunks: stats.totalChunks,
868
881
  indexStatus: stats.status,
869
882
  lastUpdated: new Date().toISOString(),
883
+ collectionName: resolvedCollectionName,
870
884
  indexFingerprint: indexFingerprint || this.runtimeFingerprint,
871
885
  fingerprintSource,
872
886
  callGraphSidecar: existing?.callGraphSidecar,
@@ -886,6 +900,7 @@ export class SnapshotManager {
886
900
  errorMessage,
887
901
  lastAttemptedPercentage,
888
902
  lastUpdated: new Date().toISOString(),
903
+ collectionName: existing?.collectionName,
889
904
  indexFingerprint: existing?.indexFingerprint,
890
905
  fingerprintSource: existing?.fingerprintSource,
891
906
  callGraphSidecar: existing?.callGraphSidecar,
@@ -897,10 +912,13 @@ export class SnapshotManager {
897
912
  this.markDirty();
898
913
  this.refreshDerivedState();
899
914
  }
900
- setCodebaseSyncCompleted(codebasePath, stats, indexFingerprint, fingerprintSource = 'verified') {
915
+ setCodebaseSyncCompleted(codebasePath, stats, indexFingerprint, fingerprintSource = 'verified', collectionName) {
901
916
  this.markCodebasePresent(codebasePath);
902
917
  const totalChanges = stats.added + stats.removed + stats.modified;
903
918
  const existing = this.codebaseInfoMap.get(codebasePath);
919
+ const resolvedCollectionName = typeof collectionName === "string" && collectionName.trim().length > 0
920
+ ? collectionName.trim()
921
+ : existing?.collectionName;
904
922
  const info = {
905
923
  status: 'sync_completed',
906
924
  added: stats.added,
@@ -908,6 +926,7 @@ export class SnapshotManager {
908
926
  modified: stats.modified,
909
927
  totalChanges,
910
928
  lastUpdated: new Date().toISOString(),
929
+ collectionName: resolvedCollectionName,
911
930
  indexFingerprint: indexFingerprint || existing?.indexFingerprint || this.runtimeFingerprint,
912
931
  fingerprintSource,
913
932
  callGraphSidecar: existing?.callGraphSidecar,
@@ -927,6 +946,7 @@ export class SnapshotManager {
927
946
  message: message || 'Index is incompatible with the current runtime fingerprint and must be rebuilt.',
928
947
  reindexReason: reason,
929
948
  lastUpdated: new Date().toISOString(),
949
+ collectionName: existing?.collectionName,
930
950
  indexFingerprint: existing?.indexFingerprint,
931
951
  fingerprintSource: existing?.fingerprintSource,
932
952
  callGraphSidecar: existing?.callGraphSidecar,
@@ -946,6 +966,7 @@ export class SnapshotManager {
946
966
  modified: 0,
947
967
  totalChanges: 0,
948
968
  lastUpdated: new Date().toISOString(),
969
+ collectionName: info.collectionName,
949
970
  indexFingerprint: info.indexFingerprint,
950
971
  fingerprintSource: info.fingerprintSource,
951
972
  callGraphSidecar: info.callGraphSidecar,
@@ -39,6 +39,7 @@ interface SyncStats {
39
39
  modified: number;
40
40
  changedFiles: string[];
41
41
  navigationRecovery?: 'rebuilt' | 'failed';
42
+ collectionName?: string;
42
43
  }
43
44
  type WatchSyncReason = 'watch_event' | 'ignore_rules_changed';
44
45
  interface EnsureFreshnessOptions {
package/dist/core/sync.js CHANGED
@@ -295,7 +295,12 @@ export class SyncManager {
295
295
  }
296
296
  try {
297
297
  // Incremental sync
298
- const stats = await this.context.reindexByChange(codebasePath);
298
+ const collectionName = this.snapshotManager.getCodebaseCollectionName?.(codebasePath);
299
+ const syncOptions = {
300
+ ...(collectionName ? { targetCollectionName: collectionName } : {}),
301
+ maintainCompletionMarker: true,
302
+ };
303
+ const stats = await this.context.reindexByChange(codebasePath, undefined, syncOptions);
299
304
  if (typeof this.context.getTrackedRelativePaths === 'function') {
300
305
  const trackedPaths = this.context.getTrackedRelativePaths(codebasePath);
301
306
  if (typeof this.snapshotManager.setCodebaseIndexManifest === 'function') {
@@ -310,7 +315,7 @@ export class SyncManager {
310
315
  return { mode: 'skipped_requires_reindex', stats };
311
316
  }
312
317
  // Persist Snapshot
313
- this.snapshotManager.setCodebaseSyncCompleted(codebasePath, stats);
318
+ this.snapshotManager.setCodebaseSyncCompleted(codebasePath, stats, undefined, 'verified', stats.collectionName || collectionName);
314
319
  this.snapshotManager.saveCodebaseSnapshot();
315
320
  if (this.onSyncCompleted) {
316
321
  await this.onSyncCompleted(codebasePath, {
@@ -23,7 +23,7 @@ type CallGraphContext = {
23
23
  limit: number;
24
24
  };
25
25
  export type ToolResponseBuildersHost = {
26
- buildManageIndexRecommendedAction(action: "create" | "reindex" | "sync" | "status", codebasePath: string, rationale: string): SearchRecommendedNextAction;
26
+ buildManageIndexRecommendedAction(action: "create" | "reindex" | "sync" | "status" | "repair", codebasePath: string, rationale: string): SearchRecommendedNextAction;
27
27
  buildCreateHint(codebasePath: string): {
28
28
  tool: string;
29
29
  args: {
@@ -38,6 +38,13 @@ export type ToolResponseBuildersHost = {
38
38
  path: string;
39
39
  };
40
40
  };
41
+ buildRepairHint(codebasePath: string): {
42
+ tool: string;
43
+ args: {
44
+ action: string;
45
+ path: string;
46
+ };
47
+ };
41
48
  buildStatusHint(codebasePath: string): {
42
49
  tool: string;
43
50
  args: {
@@ -110,6 +110,7 @@ export class ToolResponseBuilders {
110
110
  }
111
111
  buildRequiresReindexCallGraphPayload(codebasePath, detail, context, reason = "requires_reindex") {
112
112
  const detailLine = detail ? `${detail}\n\n` : "";
113
+ const preferRepair = reason === "missing_symbol_registry" || reason === "missing_relationship_sidecar";
113
114
  return {
114
115
  status: "requires_reindex",
115
116
  supported: false,
@@ -126,8 +127,11 @@ export class ToolResponseBuilders {
126
127
  freshnessDecision: {
127
128
  mode: "skipped_requires_reindex",
128
129
  },
129
- message: `${detailLine}The index at '${codebasePath}' is incompatible with the current runtime and must be rebuilt. Please run manage_index with {"action":"reindex","path":"${codebasePath}"}.`,
130
+ message: preferRepair
131
+ ? `${detailLine}The index at '${codebasePath}' is missing navigation sidecars. Please run manage_index with {"action":"repair","path":"${codebasePath}"}.`
132
+ : `${detailLine}The index at '${codebasePath}' is incompatible with the current runtime and must be rebuilt. Please run manage_index with {"action":"reindex","path":"${codebasePath}"}.`,
130
133
  hints: {
134
+ ...(preferRepair ? { repair: this.host.buildRepairHint(codebasePath) } : {}),
131
135
  reindex: this.host.buildReindexHint(codebasePath),
132
136
  },
133
137
  compatibility: this.host.buildCompatibilityDiagnostics(codebasePath),
@@ -1,6 +1,5 @@
1
1
  import { COLLECTION_LIMIT_MESSAGE, deleteCollectionWithVerification, } from "@zokizuan/satori-core";
2
2
  const SATORI_COLLECTION_PREFIXES = ["code_chunks_", "hybrid_code_chunks_"];
3
- const ZILLIZ_FREE_TIER_COLLECTION_LIMIT = 5;
4
3
  const MIN_RELIABLE_COLLECTION_CREATED_AT_MS = Date.UTC(2000, 0, 1);
5
4
  function isRecord(value) {
6
5
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -185,7 +184,7 @@ export class VectorBackendMaintenance {
185
184
  : "No Satori-managed collections were discovered.";
186
185
  return `${COLLECTION_LIMIT_MESSAGE}
187
186
 
188
- Reason: Zilliz free-tier clusters are capped at ${ZILLIZ_FREE_TIER_COLLECTION_LIMIT} collections, and this cluster has no remaining collection slots.
187
+ Reason: The connected Zilliz cluster has no remaining collection slots.
189
188
  Target codebase: '${targetCodebasePath}'
190
189
  Target collection: '${targetCollectionName}'
191
190
 
@@ -153,7 +153,10 @@ class ContextMcpServer {
153
153
  : null;
154
154
  const decision = decideInterruptedIndexingRecovery(marker, this.runtimeFingerprint);
155
155
  if (decision.action === "promote_indexed") {
156
- toolContext.snapshotManager.setCodebaseIndexed(codebasePath, decision.stats, decision.indexFingerprint, "verified");
156
+ const collectionName = typeof toolContext.context.getActiveIndexedCollectionName === "function"
157
+ ? await toolContext.context.getActiveIndexedCollectionName(codebasePath) ?? undefined
158
+ : undefined;
159
+ toolContext.snapshotManager.setCodebaseIndexed(codebasePath, decision.stats, decision.indexFingerprint, "verified", collectionName);
157
160
  promotedCount++;
158
161
  const recoveryMode = decision.reason === "valid_marker_runtime_mismatch"
159
162
  ? "indexed (fingerprint recovered from marker; current runtime differs)"
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { formatZodError } from "./types.js";
3
3
  import { classifyVectorBackendError, formatManageProviderConfigError, formatManageVectorBackendError, isMissingProviderConfigIssue } from "./setup-errors.js";
4
- const actionEnum = z.enum(["create", "reindex", "sync", "status", "clear"]);
4
+ const actionEnum = z.enum(["create", "reindex", "sync", "status", "clear", "repair"]);
5
5
  const manageIndexInputSchema = z.object({
6
6
  action: actionEnum.describe("Required operation to run."),
7
7
  path: z.string().min(1).describe("ABSOLUTE path to the target codebase."),
@@ -13,7 +13,7 @@ const manageIndexInputSchema = z.object({
13
13
  });
14
14
  export const manageIndexTool = {
15
15
  name: "manage_index",
16
- description: () => "Manage index lifecycle operations (create/reindex/sync/status/clear) for a codebase path. Ignore-rule edits in repo-root .satoriignore/.gitignore reconcile automatically in the normal sync path. Use action=\"sync\" for immediate convergence and action=\"reindex\" for full rebuild recovery (preflight may block unnecessary ignore-only reindex churn unless allowUnnecessaryReindex=true). create/reindex return the kickoff response immediately and do not poll to terminal state; use action=\"status\" to observe progress.",
16
+ description: () => "Manage index lifecycle operations (create/reindex/sync/status/clear/repair) for a codebase path. repair rebuilds local readiness only when existing vector payload and trusted runtime fingerprint proof match; otherwise it refuses and asks for create/reindex. Ignore-rule edits in repo-root .satoriignore/.gitignore reconcile automatically in the normal sync path. Use action=\"sync\" for immediate convergence and action=\"reindex\" for full rebuild recovery (preflight may block unnecessary ignore-only reindex churn unless allowUnnecessaryReindex=true). create/reindex return the kickoff response immediately and do not poll to terminal state; use action=\"status\" to observe progress.",
17
17
  inputSchemaZod: () => manageIndexInputSchema,
18
18
  execute: async (args, ctx) => {
19
19
  const parsed = manageIndexInputSchema.safeParse(args || {});
@@ -29,7 +29,7 @@ export const manageIndexTool = {
29
29
  const input = parsed.data;
30
30
  const providerOperation = input.action === "clear" || input.action === "status"
31
31
  ? "vector_only"
32
- : (input.action === "create" || input.action === "reindex" || input.action === "sync")
32
+ : (input.action === "create" || input.action === "reindex" || input.action === "sync" || input.action === "repair")
33
33
  ? "embedding_vector"
34
34
  : null;
35
35
  let executionContext;
@@ -76,6 +76,9 @@ export const manageIndexTool = {
76
76
  case 'clear':
77
77
  response = await executionContext.toolHandlers.handleClearIndex(input);
78
78
  break;
79
+ case 'repair':
80
+ response = await executionContext.toolHandlers.handleRepairIndex(input);
81
+ break;
79
82
  default:
80
83
  return {
81
84
  content: [{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zokizuan/satori-mcp",
3
- "version": "4.11.8",
3
+ "version": "4.11.13",
4
4
  "description": "Repo-aware MCP tools for AI coding agents: intent search, symbol navigation, exact reads, related paths, and freshness warnings.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -15,7 +15,7 @@
15
15
  "jsonc-parser": "^3.3.1",
16
16
  "zod": "^3.25.55",
17
17
  "zod-to-json-schema": "^3.25.1",
18
- "@zokizuan/satori-core": "1.6.4"
18
+ "@zokizuan/satori-core": "1.6.9"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@types/node": "^20.0.0",