@usewhisper/mcp-server 2.13.0 → 2.14.0

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.
Files changed (3) hide show
  1. package/README.md +5 -2
  2. package/dist/server.js +1120 -511
  3. package/package.json +2 -2
package/dist/server.js CHANGED
@@ -3785,6 +3785,9 @@ function buildMcpSearchPayload(input) {
3785
3785
  count: normalizedResults.length,
3786
3786
  degraded_mode: Boolean(input.degradedMode),
3787
3787
  degraded_reason: input.degradedReason ?? null,
3788
+ semantic_status: input.semanticStatus ?? (input.degradedMode ? "failed" : "ok"),
3789
+ fallback_mode: input.fallbackMode ?? (input.degradedMode ? "lexical_backend" : "none"),
3790
+ recommended_fixes: input.recommendedFixes || [],
3788
3791
  warnings: input.warnings || []
3789
3792
  });
3790
3793
  }
@@ -3844,6 +3847,8 @@ var RETRIEVAL_READINESS_VALUES = [
3844
3847
  "local_fallback"
3845
3848
  ];
3846
3849
  var RETRIEVAL_ROUTE_VALUES = ["project_repo", "local_workspace_fallback", "memory_only", "none"];
3850
+ var STATUS_LABEL_VALUES = ["healthy", "degraded_auto_repairing", "needs_user_action", "blocked"];
3851
+ var STATUS_ACTION_VALUES = ["none", "run_auto_repair", "restart_client", "reauth", "add_source", "reindex"];
3847
3852
  var STATE_DIR = join(homedir(), ".whisper-mcp");
3848
3853
  var STATE_PATH = join(STATE_DIR, "state.json");
3849
3854
  var AUDIT_LOG_PATH = join(STATE_DIR, "forget-audit.log");
@@ -3876,6 +3881,7 @@ var ALIAS_TOOL_MAP = [
3876
3881
  { alias: "search", target: "context.query | memory.get" },
3877
3882
  { alias: "search_code", target: "code.search_semantic" },
3878
3883
  { alias: "grep", target: "code.search_text" },
3884
+ { alias: "fix", target: "index.auto_repair" },
3879
3885
  { alias: "read", target: "local.file_read" },
3880
3886
  { alias: "explore", target: "local.tree" },
3881
3887
  { alias: "research", target: "research.oracle" },
@@ -3891,11 +3897,14 @@ var RECOMMENDED_NEXT_CALL_ALLOWLIST = /* @__PURE__ */ new Set([
3891
3897
  "context.list_projects",
3892
3898
  "index.workspace_status",
3893
3899
  "index.workspace_run",
3900
+ "index.auto_repair",
3901
+ "fix",
3894
3902
  "search_code",
3895
3903
  "grep",
3896
3904
  "context.list_sources",
3897
3905
  "context.add_source",
3898
3906
  "index.local_scan_ingest",
3907
+ "context.query",
3899
3908
  "context.get_relevant"
3900
3909
  ]);
3901
3910
  var UNTRUSTED_CODEBASE_HEALTH = /* @__PURE__ */ new Set(["unbound", "unindexed"]);
@@ -3903,31 +3912,85 @@ var UNTRUSTED_CODEBASE_PROJECT_READINESS = /* @__PURE__ */ new Set([
3903
3912
  "project_unverified",
3904
3913
  "project_bound_no_repo_source"
3905
3914
  ]);
3915
+ var RETRIEVAL_READINESS_RANK = {
3916
+ no_project: 0,
3917
+ project_unverified: 0,
3918
+ project_bound_no_repo_source: 1,
3919
+ project_repo_source_stale: 2,
3920
+ local_fallback: 3,
3921
+ project_repo_source_ready: 4
3922
+ };
3923
+ var ROOT_CAUSE_PRIMARY_ACTION = {
3924
+ none: "none",
3925
+ missing_api_key: "reauth",
3926
+ no_project_bound: "add_source",
3927
+ mode_mismatch_remote_local_ingest: "restart_client",
3928
+ workspace_unindexed: "reindex",
3929
+ project_bound_no_repo_source: "add_source",
3930
+ project_unverified: "reauth",
3931
+ project_repo_source_stale: "reindex",
3932
+ semantic_backend_unavailable: "run_auto_repair",
3933
+ local_fallback_active: "run_auto_repair"
3934
+ };
3935
+ var ROOT_CAUSE_NEXT_CALLS = {
3936
+ none: [],
3937
+ missing_api_key: [],
3938
+ no_project_bound: ["context.list_projects"],
3939
+ mode_mismatch_remote_local_ingest: [],
3940
+ workspace_unindexed: ["index.workspace_run"],
3941
+ project_bound_no_repo_source: ["context.add_source"],
3942
+ project_unverified: [],
3943
+ project_repo_source_stale: ["index.workspace_run"],
3944
+ semantic_backend_unavailable: ["fix"],
3945
+ local_fallback_active: ["fix"]
3946
+ };
3906
3947
  function sanitizeRecommendedNextCalls(nextCalls) {
3907
3948
  return Array.from(
3908
3949
  new Set(
3909
3950
  nextCalls.map((entry) => String(entry || "").trim()).filter((entry) => RECOMMENDED_NEXT_CALL_ALLOWLIST.has(entry))
3910
3951
  )
3911
- );
3952
+ ).slice(0, 3);
3953
+ }
3954
+ function recommendedNextCallsAreAcyclic(nextCalls) {
3955
+ const seen = /* @__PURE__ */ new Set();
3956
+ for (const nextCall of nextCalls) {
3957
+ if (seen.has(nextCall)) return false;
3958
+ seen.add(nextCall);
3959
+ }
3960
+ return true;
3961
+ }
3962
+ function retrievalReadinessRank(readiness) {
3963
+ return RETRIEVAL_READINESS_RANK[readiness];
3964
+ }
3965
+ function isStrictlyBetterReadiness(from, to) {
3966
+ return retrievalReadinessRank(to) > retrievalReadinessRank(from);
3967
+ }
3968
+ function statusActionForRootCause(rootCause) {
3969
+ return ROOT_CAUSE_PRIMARY_ACTION[rootCause] || "none";
3970
+ }
3971
+ function recommendedNextCallsForRootCause(rootCause) {
3972
+ return sanitizeRecommendedNextCalls(ROOT_CAUSE_NEXT_CALLS[rootCause] || []);
3973
+ }
3974
+ function statusLabelForAction(action) {
3975
+ if (action === "none") return "healthy";
3976
+ if (action === "run_auto_repair") return "degraded_auto_repairing";
3977
+ if (action === "reauth") return "blocked";
3978
+ return "needs_user_action";
3979
+ }
3980
+ function actionRequiredForAction(action) {
3981
+ return action !== "none" && action !== "run_auto_repair";
3912
3982
  }
3913
3983
  function getRepoReadinessNextCalls(readiness) {
3914
- if (readiness !== "project_bound_no_repo_source" && readiness !== "project_unverified") {
3915
- return [];
3984
+ if (readiness === "project_bound_no_repo_source") {
3985
+ return recommendedNextCallsForRootCause("project_bound_no_repo_source");
3916
3986
  }
3917
- if (RUNTIME_MODE === "remote") {
3918
- return sanitizeRecommendedNextCalls([
3919
- "context.list_sources",
3920
- "context.add_source",
3921
- "index.workspace_status",
3922
- "context.get_relevant"
3923
- ]);
3987
+ if (readiness === "project_unverified") {
3988
+ return recommendedNextCallsForRootCause("project_unverified");
3924
3989
  }
3925
- return sanitizeRecommendedNextCalls([
3926
- "context.list_sources",
3927
- "index.local_scan_ingest",
3928
- "index.workspace_status",
3929
- "context.get_relevant"
3930
- ]);
3990
+ if (readiness === "project_repo_source_stale") {
3991
+ return recommendedNextCallsForRootCause("project_repo_source_stale");
3992
+ }
3993
+ return [];
3931
3994
  }
3932
3995
  function resolveMcpRetrievalProfile(requested) {
3933
3996
  if (requested && MCP_RETRIEVAL_PROFILE_VALUES.includes(requested)) {
@@ -3935,6 +3998,156 @@ function resolveMcpRetrievalProfile(requested) {
3935
3998
  }
3936
3999
  return MCP_RETRIEVAL_PRECISION_V1_DEFAULT ? "precision_v1" : "legacy";
3937
4000
  }
4001
+ function statusContractForReadiness(args) {
4002
+ if (args.root_cause_code && args.root_cause_code !== "none") {
4003
+ const action = statusActionForRootCause(args.root_cause_code);
4004
+ return {
4005
+ status_label: statusLabelForAction(action),
4006
+ status_action: action,
4007
+ action_required: actionRequiredForAction(action)
4008
+ };
4009
+ }
4010
+ if (args.force_repairing) {
4011
+ return {
4012
+ status_label: "degraded_auto_repairing",
4013
+ status_action: "run_auto_repair",
4014
+ action_required: false
4015
+ };
4016
+ }
4017
+ if (args.semantic_status === "failed") {
4018
+ return {
4019
+ status_label: "degraded_auto_repairing",
4020
+ status_action: "run_auto_repair",
4021
+ action_required: false
4022
+ };
4023
+ }
4024
+ if (args.workspace_health === "unindexed") {
4025
+ return {
4026
+ status_label: "needs_user_action",
4027
+ status_action: "reindex",
4028
+ action_required: true
4029
+ };
4030
+ }
4031
+ if (args.retrieval_readiness === "project_unverified") {
4032
+ return {
4033
+ status_label: "blocked",
4034
+ status_action: "reauth",
4035
+ action_required: true
4036
+ };
4037
+ }
4038
+ if (args.retrieval_readiness === "project_bound_no_repo_source") {
4039
+ if ((args.runtime_mode || RUNTIME_MODE) === "remote") {
4040
+ return {
4041
+ status_label: "needs_user_action",
4042
+ status_action: "add_source",
4043
+ action_required: true
4044
+ };
4045
+ }
4046
+ return {
4047
+ status_label: "needs_user_action",
4048
+ status_action: "reindex",
4049
+ action_required: true
4050
+ };
4051
+ }
4052
+ if (args.retrieval_readiness === "project_repo_source_stale") {
4053
+ return {
4054
+ status_label: "needs_user_action",
4055
+ status_action: "reindex",
4056
+ action_required: true
4057
+ };
4058
+ }
4059
+ if (args.retrieval_readiness === "local_fallback") {
4060
+ return {
4061
+ status_label: "degraded_auto_repairing",
4062
+ status_action: "run_auto_repair",
4063
+ action_required: false
4064
+ };
4065
+ }
4066
+ if (args.retrieval_readiness === "no_project") {
4067
+ return {
4068
+ status_label: "needs_user_action",
4069
+ status_action: "add_source",
4070
+ action_required: true
4071
+ };
4072
+ }
4073
+ return {
4074
+ status_label: "healthy",
4075
+ status_action: "none",
4076
+ action_required: false
4077
+ };
4078
+ }
4079
+ function semanticStatusFromSearchMode(mode) {
4080
+ if (mode === "semantic") return "ok";
4081
+ if (mode === "adaptive_semantic") return "adaptive";
4082
+ return "failed";
4083
+ }
4084
+ function fallbackModeFromSearchMode(mode) {
4085
+ if (mode === "semantic") return "none";
4086
+ if (mode === "adaptive_semantic") return "adaptive_threshold";
4087
+ return "lexical_rescue";
4088
+ }
4089
+ function parsePositiveNumber(value, fallback = 0) {
4090
+ const parsed = Number(value);
4091
+ if (!Number.isFinite(parsed) || parsed < 0) return fallback;
4092
+ return parsed;
4093
+ }
4094
+ function getWorkspaceSemanticFailureStats(workspaceId) {
4095
+ const state = loadState();
4096
+ const workspace = getWorkspaceState(state, workspaceId);
4097
+ const failures = parsePositiveNumber(workspace.index_metadata?.semantic_failures, 0);
4098
+ const attempts = parsePositiveNumber(workspace.index_metadata?.semantic_attempts, 0);
4099
+ const rate = attempts > 0 ? failures / attempts : 0;
4100
+ return { failures, attempts, rate };
4101
+ }
4102
+ function recordSemanticAttempt(workspaceId, failed) {
4103
+ const state = loadState();
4104
+ const workspace = getWorkspaceState(state, workspaceId);
4105
+ if (!workspace.index_metadata) workspace.index_metadata = {};
4106
+ const failures = parsePositiveNumber(workspace.index_metadata.semantic_failures, 0);
4107
+ const attempts = parsePositiveNumber(workspace.index_metadata.semantic_attempts, 0);
4108
+ workspace.index_metadata.semantic_attempts = attempts + 1;
4109
+ workspace.index_metadata.semantic_failures = failed ? failures + 1 : failures;
4110
+ saveState(state);
4111
+ }
4112
+ function computeTrustScore(args) {
4113
+ let score = 100;
4114
+ if (args.workspace_health === "unbound" || args.workspace_health === "unindexed") score -= 35;
4115
+ if (args.workspace_health === "stale" || args.workspace_health === "drifted") score -= 18;
4116
+ if (args.retrieval_readiness === "project_unverified") score -= 35;
4117
+ if (args.retrieval_readiness === "project_bound_no_repo_source") score -= 32;
4118
+ if (args.retrieval_readiness === "project_repo_source_stale") score -= 20;
4119
+ if (args.retrieval_readiness === "local_fallback") score -= 10;
4120
+ const failureRate = Math.max(0, Math.min(1, args.semantic_failure_rate || 0));
4121
+ score -= Math.round(failureRate * 25);
4122
+ return Math.max(0, Math.min(100, score));
4123
+ }
4124
+ function persistTrustScore(workspaceId, trustScore) {
4125
+ const state = loadState();
4126
+ const workspace = getWorkspaceState(state, workspaceId);
4127
+ if (!workspace.index_metadata) workspace.index_metadata = {};
4128
+ workspace.index_metadata.trust_score = Math.max(0, Math.min(100, Math.round(trustScore)));
4129
+ saveState(state);
4130
+ }
4131
+ function shouldAutoHeal(args) {
4132
+ if (args.retrieval_readiness === "project_unverified" || args.retrieval_readiness === "project_bound_no_repo_source") {
4133
+ return true;
4134
+ }
4135
+ if ((args.semantic_attempts || 0) >= 3 && args.semantic_failure_rate >= 0.5) {
4136
+ return true;
4137
+ }
4138
+ return args.trust_score < 60;
4139
+ }
4140
+ function resolveAutoRepairRootCause(args) {
4141
+ if (args.restart_required) return "mode_mismatch_remote_local_ingest";
4142
+ if (args.trust_health === "unindexed") return "workspace_unindexed";
4143
+ if (args.project_readiness === "project_bound_no_repo_source") return "project_bound_no_repo_source";
4144
+ if (args.project_readiness === "project_unverified") return "project_unverified";
4145
+ if (args.project_readiness === "project_repo_source_stale") return "project_repo_source_stale";
4146
+ if (args.semantic_attempts >= 3 && args.semantic_failure_rate >= 0.5) return "semantic_backend_unavailable";
4147
+ if (args.retrieval_readiness === "local_fallback") return "local_fallback_active";
4148
+ if (args.retrieval_readiness === "no_project") return "no_project_bound";
4149
+ return "none";
4150
+ }
3938
4151
  function shouldAbstainForCodebaseTrust(args) {
3939
4152
  if (args.retrievalProfile !== "precision_v1" || !args.codebaseIntent) return false;
3940
4153
  return UNTRUSTED_CODEBASE_HEALTH.has(args.preflight.trust_state.health) || UNTRUSTED_CODEBASE_PROJECT_READINESS.has(args.preflight.project_retrieval_readiness);
@@ -4270,9 +4483,9 @@ async function resolveProjectDescriptor(projectRef) {
4270
4483
  }
4271
4484
  }
4272
4485
  function getWorkspaceRecommendedNextCalls(health) {
4273
- if (health === "unbound") return sanitizeRecommendedNextCalls(["index.workspace_resolve", "context.list_projects", "index.workspace_status"]);
4274
- if (health === "unindexed") return sanitizeRecommendedNextCalls(["index.workspace_status", "index.workspace_run", "search_code", "grep"]);
4275
- if (health === "stale" || health === "drifted") return sanitizeRecommendedNextCalls(["index.workspace_status", "index.workspace_run", "grep", "search_code"]);
4486
+ if (health === "unbound") return sanitizeRecommendedNextCalls(["index.workspace_resolve"]);
4487
+ if (health === "unindexed") return sanitizeRecommendedNextCalls(["index.workspace_run"]);
4488
+ if (health === "stale" || health === "drifted") return sanitizeRecommendedNextCalls(["index.workspace_run"]);
4276
4489
  return [];
4277
4490
  }
4278
4491
  function getWorkspaceWarnings(args) {
@@ -4468,7 +4681,7 @@ async function resolveRepoGroundingPreflight(args) {
4468
4681
  }
4469
4682
  }
4470
4683
  const recommendedNextCalls = [...trust.recommended_next_calls];
4471
- if (sourceVerification.retrieval_readiness === "project_bound_no_repo_source" || sourceVerification.retrieval_readiness === "project_unverified") {
4684
+ if (sourceVerification.retrieval_readiness !== "project_repo_source_ready" && sourceVerification.retrieval_readiness !== "no_project") {
4472
4685
  recommendedNextCalls.push(...getRepoReadinessNextCalls(sourceVerification.retrieval_readiness));
4473
4686
  }
4474
4687
  return {
@@ -4498,53 +4711,32 @@ async function ingestSessionWithSyncFallback(params) {
4498
4711
  return whisper.ingestSession(params);
4499
4712
  }
4500
4713
  }
4501
- function defaultMcpUserId() {
4714
+ function defaultMcpUserId(params) {
4502
4715
  const explicit = process.env.WHISPER_USER_ID?.trim();
4503
4716
  if (explicit) return explicit;
4504
- const seed = `${process.cwd()}|${DEFAULT_PROJECT || "default"}|${API_KEY.slice(0, 12) || "anon"}`;
4717
+ const workspacePath = params?.workspacePath || canonicalizeWorkspacePath(process.cwd());
4718
+ const projectRef = params?.project || DEFAULT_PROJECT || "default";
4719
+ const seed = `${workspacePath}|${projectRef}|${API_KEY.slice(0, 12) || "anon"}`;
4505
4720
  return `mcp-user-${createHash("sha256").update(seed).digest("hex").slice(0, 12)}`;
4506
4721
  }
4507
- function resolveMcpScope(params) {
4722
+ function resolveMcpScope(params, options) {
4723
+ const workspacePath = canonicalizeWorkspacePath(params?.path || process.env.WHISPER_WORKSPACE_PATH || process.cwd());
4724
+ const explicitSession = params?.session_id?.trim();
4508
4725
  return {
4509
4726
  project: params?.project,
4510
- userId: params?.user_id?.trim() || defaultMcpUserId(),
4511
- sessionId: params?.session_id?.trim() || cachedMcpSessionId,
4512
- workspacePath: canonicalizeWorkspacePath(params?.path || process.env.WHISPER_WORKSPACE_PATH || process.cwd())
4727
+ userId: params?.user_id?.trim() || defaultMcpUserId({
4728
+ project: params?.project,
4729
+ workspacePath
4730
+ }),
4731
+ sessionId: explicitSession || (options?.include_default_session ? cachedMcpSessionId : void 0),
4732
+ workspacePath
4513
4733
  };
4514
4734
  }
4515
- async function prepareAutomaticQuery(params) {
4516
- if (!runtimeClient) {
4517
- throw new Error("Whisper runtime client unavailable.");
4518
- }
4519
- const scope = resolveMcpScope(params);
4520
- const key = [
4521
- params.project || DEFAULT_PROJECT || "",
4522
- scope.userId,
4523
- scope.sessionId,
4524
- scope.workspacePath || process.cwd(),
4525
- String(params.top_k || 10)
4526
- ].join("|");
4527
- let runtime = runtimeSessions.get(key);
4528
- if (!runtime) {
4529
- runtime = runtimeClient.createAgentRuntime({
4530
- project: params.project,
4531
- userId: scope.userId,
4532
- sessionId: scope.sessionId,
4533
- workspacePath: scope.workspacePath,
4534
- topK: params.top_k,
4535
- clientName: "whisper-mcp"
4536
- });
4537
- runtimeSessions.set(key, runtime);
4538
- }
4539
- return runtime.beforeTurn({
4540
- userMessage: params.query
4541
- });
4542
- }
4543
4735
  function noteAutomaticSourceActivity(params) {
4544
4736
  if (!runtimeClient) return;
4545
4737
  const sourceIds = [...new Set((params.sourceIds || []).map((value) => String(value || "").trim()).filter(Boolean))];
4546
4738
  if (sourceIds.length === 0) return;
4547
- const scope = resolveMcpScope(params);
4739
+ const scope = resolveMcpScope(params, { include_default_session: true });
4548
4740
  const key = [
4549
4741
  params.project || DEFAULT_PROJECT || "",
4550
4742
  scope.userId,
@@ -4576,7 +4768,7 @@ function buildAbstain(args) {
4576
4768
  warnings: args.warnings || [],
4577
4769
  trust_state: args.trust_state,
4578
4770
  recommended_next_calls: sanitizeRecommendedNextCalls(
4579
- args.recommended_next_calls || ["index.workspace_status", "index.workspace_run", "grep", "context.get_relevant"]
4771
+ args.recommended_next_calls || ["index.workspace_status", "index.workspace_run", "fix", "context.query"]
4580
4772
  ),
4581
4773
  diagnostics: {
4582
4774
  claims_evaluated: args.claims_evaluated,
@@ -4631,6 +4823,415 @@ function countCodeFiles(searchPath, maxFiles = 5e3) {
4631
4823
  walk(searchPath);
4632
4824
  return { total, skipped };
4633
4825
  }
4826
+ function computeWorkspaceSnapshot(searchPath, maxFiles = 250) {
4827
+ const files = collectCodeFiles(searchPath, CODE_EXTENSIONS, maxFiles);
4828
+ const entries = files.slice(0, maxFiles).map((filePath) => {
4829
+ try {
4830
+ const st = statSync(filePath);
4831
+ return `${relative(searchPath, filePath)}:${st.mtimeMs}:${st.size}`;
4832
+ } catch {
4833
+ return "";
4834
+ }
4835
+ }).filter(Boolean);
4836
+ const sample = entries.join("|");
4837
+ return {
4838
+ hash: createHash("sha256").update(sample || searchPath).digest("hex").slice(0, 24),
4839
+ entries
4840
+ };
4841
+ }
4842
+ function computeSnapshotChangedFiles(previousEntries, nextEntries) {
4843
+ if (!previousEntries || previousEntries.length === 0) return nextEntries.length;
4844
+ const previousByPath = /* @__PURE__ */ new Map();
4845
+ for (const entry of previousEntries) {
4846
+ const idx = entry.indexOf(":");
4847
+ if (idx <= 0) continue;
4848
+ previousByPath.set(entry.slice(0, idx), entry.slice(idx + 1));
4849
+ }
4850
+ let changed = 0;
4851
+ const seen = /* @__PURE__ */ new Set();
4852
+ for (const entry of nextEntries) {
4853
+ const idx = entry.indexOf(":");
4854
+ if (idx <= 0) continue;
4855
+ const relPath = entry.slice(0, idx);
4856
+ const fingerprint = entry.slice(idx + 1);
4857
+ seen.add(relPath);
4858
+ if (previousByPath.get(relPath) !== fingerprint) changed += 1;
4859
+ }
4860
+ for (const relPath of previousByPath.keys()) {
4861
+ if (!seen.has(relPath)) changed += 1;
4862
+ }
4863
+ return changed;
4864
+ }
4865
+ function shouldTriggerBackgroundRefresh(args) {
4866
+ const nowMs = args.now_ms ?? Date.now();
4867
+ const lastAutoRefreshAt = args.last_auto_refresh_at ? new Date(args.last_auto_refresh_at).getTime() : 0;
4868
+ const debounceMs = 10 * 60 * 1e3;
4869
+ const dueForDebounce = !lastAutoRefreshAt || Number.isNaN(lastAutoRefreshAt) || nowMs - lastAutoRefreshAt >= debounceMs;
4870
+ if (!dueForDebounce) {
4871
+ return { should_refresh: false, trigger_reason: null };
4872
+ }
4873
+ if (args.last_indexed_commit && args.current_head && args.last_indexed_commit !== args.current_head) {
4874
+ return { should_refresh: true, trigger_reason: "head_changed" };
4875
+ }
4876
+ if (args.changed_files >= 20) {
4877
+ return { should_refresh: true, trigger_reason: "delta_files_threshold" };
4878
+ }
4879
+ const ratio = args.indexed_files > 0 ? args.changed_files / args.indexed_files : 0;
4880
+ if (ratio >= 0.02) {
4881
+ return { should_refresh: true, trigger_reason: "delta_ratio_threshold" };
4882
+ }
4883
+ return { should_refresh: false, trigger_reason: null };
4884
+ }
4885
+ async function runBackgroundWorkspaceRefresh(args) {
4886
+ const state = loadState();
4887
+ const workspace = getWorkspaceState(state, args.workspace_id);
4888
+ if (!workspace.index_metadata) workspace.index_metadata = {};
4889
+ const fileStats = countCodeFiles(args.root_path, args.max_files || 1500);
4890
+ const currentHead = getGitHead(args.root_path) || null;
4891
+ const currentSnapshot = computeWorkspaceSnapshot(args.root_path);
4892
+ const gitPendingCount = getGitPendingCount(args.root_path);
4893
+ const changedFiles = gitPendingCount == null ? computeSnapshotChangedFiles(workspace.index_metadata.last_snapshot_entries, currentSnapshot.entries) : gitPendingCount;
4894
+ const lastIndexedCommit = workspace.index_metadata.last_indexed_commit || null;
4895
+ const refreshDecision = shouldTriggerBackgroundRefresh({
4896
+ current_head: currentHead,
4897
+ last_indexed_commit: lastIndexedCommit,
4898
+ changed_files: changedFiles,
4899
+ indexed_files: parsePositiveNumber(workspace.index_metadata.indexed_files, fileStats.total || 1),
4900
+ last_auto_refresh_at: workspace.index_metadata.last_auto_refresh_at || null
4901
+ });
4902
+ const forceFullRequested = args.force_full === true;
4903
+ if (!refreshDecision.should_refresh && !forceFullRequested) {
4904
+ return { refreshed: false, mode: null, reason: null };
4905
+ }
4906
+ const mode = forceFullRequested ? "full" : "incremental";
4907
+ workspace.root_path = args.root_path;
4908
+ workspace.index_metadata.last_indexed_at = (/* @__PURE__ */ new Date()).toISOString();
4909
+ workspace.index_metadata.last_auto_refresh_at = (/* @__PURE__ */ new Date()).toISOString();
4910
+ workspace.index_metadata.last_indexed_commit = currentHead || void 0;
4911
+ workspace.index_metadata.coverage = mode === "full" ? 1 : Math.max(0, Math.min(1, fileStats.total / Math.max(1, args.max_files || 1500)));
4912
+ workspace.index_metadata.indexed_files = fileStats.total;
4913
+ workspace.index_metadata.last_snapshot_hash = currentSnapshot.hash;
4914
+ workspace.index_metadata.last_snapshot_entries = currentSnapshot.entries;
4915
+ if (mode === "full") workspace.index_metadata.last_full_refresh_at = (/* @__PURE__ */ new Date()).toISOString();
4916
+ saveState(state);
4917
+ return {
4918
+ refreshed: true,
4919
+ mode,
4920
+ reason: refreshDecision.trigger_reason || (forceFullRequested ? "manual_full_override" : mode === "full" ? "full_refresh_due" : "manual")
4921
+ };
4922
+ }
4923
+ function buildSourceRepairHint(args) {
4924
+ const remote = parseGitHubRemote(getGitRemoteUrl(args.root_path) || null);
4925
+ if (remote) {
4926
+ const branch = getGitBranch(args.root_path) || "main";
4927
+ return {
4928
+ command: `context.add_source --project ${args.project_ref || "<project>"} --type github --owner ${remote.owner} --repo ${remote.repo} --branch ${branch}`,
4929
+ message: "Attach the matching GitHub source, then rerun fix."
4930
+ };
4931
+ }
4932
+ return {
4933
+ command: `context.add_source --project ${args.project_ref || "<project>"} --type local --path "${args.root_path}"`,
4934
+ message: "Attach the local workspace as a source, then rerun fix."
4935
+ };
4936
+ }
4937
+ async function attemptDeterministicSourceAttach(args) {
4938
+ const remote = parseGitHubRemote(getGitRemoteUrl(args.root_path) || null);
4939
+ if (remote) {
4940
+ const branch = getGitBranch(args.root_path) || "main";
4941
+ const result = await createSourceByType({
4942
+ project: args.project,
4943
+ type: "github",
4944
+ name: `auto-repair:${remote.owner}/${remote.repo}`,
4945
+ owner: remote.owner,
4946
+ repo: remote.repo,
4947
+ branch,
4948
+ auto_index: true
4949
+ });
4950
+ const sourceId = result?.source_id || result?.id;
4951
+ noteAutomaticSourceActivity({
4952
+ project: args.project,
4953
+ sourceIds: sourceId ? [String(sourceId)] : []
4954
+ });
4955
+ return { attached: true, detail: `github:${remote.owner}/${remote.repo}@${branch}` };
4956
+ }
4957
+ if (RUNTIME_MODE !== "remote") {
4958
+ await createSourceByType({
4959
+ project: args.project,
4960
+ type: "local",
4961
+ name: "auto-repair:local-workspace",
4962
+ path: args.root_path,
4963
+ auto_index: true,
4964
+ max_files: 500
4965
+ });
4966
+ return { attached: true, detail: "local_workspace_source_attached" };
4967
+ }
4968
+ return { attached: false, detail: "deterministic_source_inputs_unavailable" };
4969
+ }
4970
+ async function runAutoRepairVerification(args) {
4971
+ if (args.preflight.retrieval_route === "project_repo" && args.preflight.matched_source_ids.length > 0) {
4972
+ try {
4973
+ const queryResult = await queryWithDegradedFallback({
4974
+ project: args.project,
4975
+ query: "how does this project work",
4976
+ top_k: 5,
4977
+ include_memories: false,
4978
+ include_graph: false,
4979
+ source_ids: args.preflight.matched_source_ids
4980
+ });
4981
+ const resultCount = Array.isArray(queryResult.response?.results) ? queryResult.response.results.length : 0;
4982
+ return {
4983
+ passed: resultCount > 0,
4984
+ route: "project_repo",
4985
+ result_count: resultCount,
4986
+ detail: resultCount > 0 ? "project_query_has_results" : "project_query_empty"
4987
+ };
4988
+ } catch (error) {
4989
+ return {
4990
+ passed: false,
4991
+ route: "project_repo",
4992
+ result_count: 0,
4993
+ detail: `project_query_error:${String(error?.message || error)}`
4994
+ };
4995
+ }
4996
+ }
4997
+ if (args.preflight.retrieval_route === "local_workspace_fallback") {
4998
+ try {
4999
+ const local = await runLocalWorkspaceRetrieval({
5000
+ query: "how does this project work",
5001
+ path: args.preflight.trust_state.root_path,
5002
+ project: args.project,
5003
+ top_k: 5
5004
+ });
5005
+ return {
5006
+ passed: local.results.length > 0,
5007
+ route: "local_workspace_fallback",
5008
+ result_count: local.results.length,
5009
+ detail: local.results.length > 0 ? "local_query_has_results" : "local_query_empty"
5010
+ };
5011
+ } catch (error) {
5012
+ return {
5013
+ passed: false,
5014
+ route: "local_workspace_fallback",
5015
+ result_count: 0,
5016
+ detail: `local_query_error:${String(error?.message || error)}`
5017
+ };
5018
+ }
5019
+ }
5020
+ return {
5021
+ passed: false,
5022
+ route: args.preflight.retrieval_route,
5023
+ result_count: 0,
5024
+ detail: "no_verifiable_retrieval_route"
5025
+ };
5026
+ }
5027
+ async function runAutoRepairFlow(input) {
5028
+ const actions_attempted = [];
5029
+ const actions_succeeded = [];
5030
+ const actions_failed = [];
5031
+ const apply = input.apply !== false;
5032
+ const repairScope = input.repair_scope || "safe";
5033
+ const allowRestartHint = input.allow_restart_hint !== false;
5034
+ let restartRequired = false;
5035
+ if (!API_KEY) {
5036
+ return {
5037
+ root_cause_code: "missing_api_key",
5038
+ actions_attempted,
5039
+ actions_succeeded,
5040
+ actions_failed: [...actions_failed, "validate_credentials:missing_api_key"],
5041
+ restart_required: false,
5042
+ verification_passed: false,
5043
+ status_label: "blocked",
5044
+ status_action: "reauth",
5045
+ action_required: true,
5046
+ diagnostics: {
5047
+ message: "WHISPER_API_KEY is required."
5048
+ }
5049
+ };
5050
+ }
5051
+ actions_attempted.push("validate_credentials");
5052
+ actions_succeeded.push("validate_credentials");
5053
+ actions_attempted.push("resolve_project");
5054
+ const resolvedProject = await resolveProjectRef(input.project);
5055
+ if (!resolvedProject) {
5056
+ actions_failed.push("resolve_project:no_project");
5057
+ return {
5058
+ root_cause_code: "no_project_bound",
5059
+ actions_attempted,
5060
+ actions_succeeded,
5061
+ actions_failed,
5062
+ restart_required: false,
5063
+ verification_passed: false,
5064
+ status_label: "needs_user_action",
5065
+ status_action: "add_source",
5066
+ action_required: true,
5067
+ diagnostics: {
5068
+ message: "No project resolved. Set WHISPER_PROJECT or pass project."
5069
+ }
5070
+ };
5071
+ }
5072
+ actions_succeeded.push("resolve_project");
5073
+ actions_attempted.push("resolve_workspace_trust");
5074
+ const beforePreflight = await resolveRepoGroundingPreflight({
5075
+ query: "how does this project work",
5076
+ path: input.path,
5077
+ workspace_id: input.workspace_id,
5078
+ project: resolvedProject
5079
+ });
5080
+ actions_succeeded.push("resolve_workspace_trust");
5081
+ actions_attempted.push("runtime_mode_check");
5082
+ if (allowRestartHint && RUNTIME_MODE === "remote" && (beforePreflight.project_retrieval_readiness === "project_bound_no_repo_source" || beforePreflight.retrieval_readiness === "local_fallback")) {
5083
+ actions_failed.push("runtime_mode_check:remote_mode_blocks_local_repair");
5084
+ restartRequired = true;
5085
+ } else {
5086
+ actions_succeeded.push("runtime_mode_check");
5087
+ }
5088
+ actions_attempted.push("refresh_workspace_index_metadata");
5089
+ let refreshResult = null;
5090
+ try {
5091
+ if (apply) {
5092
+ refreshResult = await runBackgroundWorkspaceRefresh({
5093
+ workspace_id: beforePreflight.trust_state.workspace_id,
5094
+ root_path: beforePreflight.trust_state.root_path,
5095
+ force_full: repairScope === "full"
5096
+ });
5097
+ } else {
5098
+ refreshResult = { refreshed: false, mode: null, reason: "dry_run" };
5099
+ }
5100
+ actions_succeeded.push("refresh_workspace_index_metadata");
5101
+ } catch (error) {
5102
+ actions_failed.push(`refresh_workspace_index_metadata:${String(error?.message || error)}`);
5103
+ }
5104
+ actions_attempted.push("repair_source_readiness");
5105
+ let sourceRepair = {
5106
+ applied: false,
5107
+ attached: false,
5108
+ detail: "not_required",
5109
+ hint: null
5110
+ };
5111
+ if (beforePreflight.project_retrieval_readiness === "project_bound_no_repo_source") {
5112
+ const hint = buildSourceRepairHint({
5113
+ root_path: beforePreflight.trust_state.root_path,
5114
+ project_ref: beforePreflight.trust_state.project_ref
5115
+ });
5116
+ sourceRepair = {
5117
+ applied: apply && repairScope === "full",
5118
+ attached: false,
5119
+ detail: "manual_source_attach_required",
5120
+ hint
5121
+ };
5122
+ if (apply && repairScope === "full") {
5123
+ try {
5124
+ const attach = await attemptDeterministicSourceAttach({
5125
+ project: resolvedProject,
5126
+ root_path: beforePreflight.trust_state.root_path
5127
+ });
5128
+ sourceRepair = {
5129
+ applied: true,
5130
+ attached: attach.attached,
5131
+ detail: attach.detail,
5132
+ hint
5133
+ };
5134
+ if (attach.attached) {
5135
+ actions_succeeded.push("repair_source_readiness");
5136
+ } else {
5137
+ actions_failed.push(`repair_source_readiness:${attach.detail}`);
5138
+ }
5139
+ } catch (error) {
5140
+ actions_failed.push(`repair_source_readiness:${String(error?.message || error)}`);
5141
+ }
5142
+ } else {
5143
+ actions_failed.push("repair_source_readiness:manual_source_attach_required");
5144
+ }
5145
+ } else {
5146
+ actions_succeeded.push("repair_source_readiness");
5147
+ }
5148
+ const afterPreflight = await resolveRepoGroundingPreflight({
5149
+ query: "how does this project work",
5150
+ path: beforePreflight.trust_state.root_path,
5151
+ workspace_id: beforePreflight.trust_state.workspace_id,
5152
+ project: resolvedProject
5153
+ });
5154
+ const semanticStats = getWorkspaceSemanticFailureStats(afterPreflight.trust_state.workspace_id);
5155
+ const trustScore = computeTrustScore({
5156
+ retrieval_readiness: afterPreflight.retrieval_readiness,
5157
+ workspace_health: afterPreflight.trust_state.health,
5158
+ semantic_failure_rate: semanticStats.rate
5159
+ });
5160
+ persistTrustScore(afterPreflight.trust_state.workspace_id, trustScore);
5161
+ const autoHeal = shouldAutoHeal({
5162
+ retrieval_readiness: afterPreflight.retrieval_readiness,
5163
+ trust_score: trustScore,
5164
+ semantic_failure_rate: semanticStats.rate,
5165
+ semantic_attempts: semanticStats.attempts
5166
+ });
5167
+ actions_attempted.push("verification_query");
5168
+ const verification = await runAutoRepairVerification({
5169
+ project: resolvedProject,
5170
+ preflight: afterPreflight
5171
+ });
5172
+ if (verification.passed) actions_succeeded.push("verification_query");
5173
+ else actions_failed.push(`verification_query:${verification.detail}`);
5174
+ const rootCause = resolveAutoRepairRootCause({
5175
+ restart_required: restartRequired,
5176
+ trust_health: beforePreflight.trust_state.health,
5177
+ project_readiness: beforePreflight.project_retrieval_readiness,
5178
+ retrieval_readiness: beforePreflight.retrieval_readiness,
5179
+ semantic_failure_rate: semanticStats.rate,
5180
+ semantic_attempts: semanticStats.attempts
5181
+ });
5182
+ const statusContract = statusContractForReadiness({
5183
+ retrieval_readiness: afterPreflight.retrieval_readiness,
5184
+ workspace_health: afterPreflight.trust_state.health,
5185
+ root_cause_code: rootCause,
5186
+ force_repairing: autoHeal && rootCause === "none"
5187
+ });
5188
+ const verificationPassed = verification.passed;
5189
+ const effectiveStatusAction = statusContract.status_action;
5190
+ const effectiveActionRequired = statusContract.action_required;
5191
+ const transitionImproved = isStrictlyBetterReadiness(
5192
+ beforePreflight.retrieval_readiness,
5193
+ afterPreflight.retrieval_readiness
5194
+ );
5195
+ const explicitBlocker = effectiveActionRequired;
5196
+ const recommendedNextCalls = sanitizeRecommendedNextCalls([
5197
+ ...recommendedNextCallsForRootCause(rootCause),
5198
+ ...afterPreflight.recommended_next_calls
5199
+ ]);
5200
+ return {
5201
+ root_cause_code: rootCause,
5202
+ actions_attempted,
5203
+ actions_succeeded,
5204
+ actions_failed,
5205
+ restart_required: restartRequired,
5206
+ verification_passed: verificationPassed,
5207
+ status_label: statusContract.status_label,
5208
+ status_action: effectiveStatusAction,
5209
+ action_required: effectiveActionRequired,
5210
+ recommended_next_calls: recommendedNextCalls,
5211
+ diagnostics: {
5212
+ runtime_mode: RUNTIME_MODE,
5213
+ workspace_id: afterPreflight.trust_state.workspace_id,
5214
+ root_path: afterPreflight.trust_state.root_path,
5215
+ project_ref: afterPreflight.trust_state.project_ref,
5216
+ retrieval_readiness_before: beforePreflight.retrieval_readiness,
5217
+ retrieval_readiness_after: afterPreflight.retrieval_readiness,
5218
+ project_retrieval_readiness_after: afterPreflight.project_retrieval_readiness,
5219
+ retrieval_readiness_label: retrievalReadinessLabel(afterPreflight.retrieval_readiness),
5220
+ retrieval_readiness_action: retrievalReadinessAction(afterPreflight.retrieval_readiness),
5221
+ trust_score: trustScore,
5222
+ semantic_failure_rate: semanticStats.rate,
5223
+ semantic_attempts: semanticStats.attempts,
5224
+ refresh: refreshResult,
5225
+ source_repair: sourceRepair,
5226
+ verification,
5227
+ auto_heal_triggered: autoHeal,
5228
+ transition_improved: transitionImproved,
5229
+ explicit_blocker: explicitBlocker,
5230
+ recommended_next_calls: recommendedNextCalls,
5231
+ restart_hint: restartRequired ? "Set WHISPER_MCP_MODE=auto in MCP config and restart client." : null
5232
+ }
5233
+ };
5234
+ }
4634
5235
  function toTextResult(payload) {
4635
5236
  return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
4636
5237
  }
@@ -4697,42 +5298,6 @@ function resolveForgetQueryCandidates(rawResults, query) {
4697
5298
  }
4698
5299
  return { memory_ids: [], resolved_by: "none", warning: "Query did not resolve to a reliable memory match. No memories were changed." };
4699
5300
  }
4700
- async function searchMemoriesForContextQuery(args) {
4701
- return whisper.searchMemoriesSOTA({
4702
- project: args.project,
4703
- query: args.query,
4704
- user_id: args.user_id,
4705
- session_id: args.session_id,
4706
- top_k: args.top_k ?? 5,
4707
- include_relations: false,
4708
- include_pending: true
4709
- });
4710
- }
4711
- async function runContextQueryMemoryRescue(args) {
4712
- const scoped = await searchMemoriesForContextQuery(args);
4713
- const scopedResults = formatCanonicalMemoryResults(scoped);
4714
- if (scopedResults.length > 0) {
4715
- return { results: scopedResults, rescue_mode: "scoped" };
4716
- }
4717
- if (args.user_id || args.session_id) {
4718
- return { results: [], rescue_mode: null };
4719
- }
4720
- const broad = await searchMemoriesForContextQuery({
4721
- project: args.project,
4722
- query: args.query,
4723
- top_k: args.top_k
4724
- });
4725
- return { results: formatCanonicalMemoryResults(broad), rescue_mode: formatCanonicalMemoryResults(broad).length > 0 ? "project_broad" : null };
4726
- }
4727
- function renderContextQueryMemoryRescue(args) {
4728
- const lines = args.results.map(
4729
- (result, index) => `${index + 1}. [${result.memory_type || "memory"}, score: ${result.similarity ?? "n/a"}] ${result.content}`
4730
- );
4731
- const scopeLabel = args.rescue_mode === "project_broad" ? `project=${args.project}, project_broad_memory_rescue=true` : `project=${args.project}, user=${args.scope.userId}, session=${args.scope.sessionId}, scoped_memory_rescue=true`;
4732
- return `Found ${args.results.length} memory result(s) (${scopeLabel}):
4733
-
4734
- ${lines.join("\n\n")}`;
4735
- }
4736
5301
  function likelyEmbeddingFailure(error) {
4737
5302
  const message = String(error?.message || error || "").toLowerCase();
4738
5303
  return message.includes("embedding") || message.includes("vector") || message.includes("timeout") || message.includes("timed out") || message.includes("temporarily unavailable");
@@ -4753,7 +5318,13 @@ async function queryWithDegradedFallback(params) {
4753
5318
  hybrid: true,
4754
5319
  rerank: true
4755
5320
  });
4756
- return { response, degraded_mode: false };
5321
+ return {
5322
+ response,
5323
+ degraded_mode: false,
5324
+ semantic_status: "ok",
5325
+ fallback_mode: "none",
5326
+ recommended_fixes: []
5327
+ };
4757
5328
  } catch (error) {
4758
5329
  if (!likelyEmbeddingFailure(error)) throw error;
4759
5330
  const response = await whisper.query({
@@ -4776,7 +5347,13 @@ async function queryWithDegradedFallback(params) {
4776
5347
  response,
4777
5348
  degraded_mode: true,
4778
5349
  degraded_reason: "Embedding/graph path unavailable; lexical fallback used.",
4779
- recommendation: "Check embedding service health, then re-run for full hybrid quality."
5350
+ recommendation: "Check embedding service health, then re-run for full hybrid quality.",
5351
+ semantic_status: "failed",
5352
+ fallback_mode: "lexical_backend",
5353
+ recommended_fixes: [
5354
+ "Run fix to auto-repair retrieval health.",
5355
+ "If this persists, re-authenticate and reindex workspace sources."
5356
+ ]
4780
5357
  };
4781
5358
  }
4782
5359
  }
@@ -4888,6 +5465,47 @@ async function runSearchCodeSearch(args) {
4888
5465
  fallback_reason: lexicalResults.length ? "Semantic ranking returned no matches; lexical rescue over local file paths and content was used." : "Semantic ranking returned no matches and lexical rescue found no strong candidates."
4889
5466
  };
4890
5467
  }
5468
+ function retrievalReadinessLabel(readiness) {
5469
+ if (readiness === "project_repo_source_ready") return "Project retrieval is healthy.";
5470
+ if (readiness === "project_repo_source_stale") return "Project source exists but is not ready.";
5471
+ if (readiness === "project_bound_no_repo_source") return "No matching project source is connected.";
5472
+ if (readiness === "project_unverified") return "Project/source verification failed.";
5473
+ if (readiness === "local_fallback") return "Using local fallback retrieval.";
5474
+ if (readiness === "no_project") return "No project is bound to this workspace.";
5475
+ return "Retrieval readiness is unknown.";
5476
+ }
5477
+ function retrievalReadinessAction(readiness) {
5478
+ if (readiness === "project_repo_source_ready") return "none";
5479
+ if (readiness === "project_repo_source_stale") return "Run fix or index.workspace_run to refresh workspace trust.";
5480
+ if (readiness === "project_bound_no_repo_source") {
5481
+ return RUNTIME_MODE === "remote" ? "Connect a repo source with context.add_source, then run fix." : "Run index.local_scan_ingest or fix.";
5482
+ }
5483
+ if (readiness === "project_unverified") return "Re-authenticate your API key and run fix.";
5484
+ if (readiness === "local_fallback") return "Run fix to restore project-grounded retrieval.";
5485
+ if (readiness === "no_project") return "Provide --project/WHISPER_PROJECT and run fix.";
5486
+ return "Run fix.";
5487
+ }
5488
+ function buildRecommendedFixes(args) {
5489
+ const fixes = /* @__PURE__ */ new Set();
5490
+ if (args.semantic_status === "failed") {
5491
+ fixes.add("Run fix to auto-repair retrieval health.");
5492
+ }
5493
+ if (args.fallback_mode === "lexical_rescue" || args.fallback_mode === "lexical_backend") {
5494
+ fixes.add("Check embedding/vector service health and retry.");
5495
+ }
5496
+ if (args.retrieval_readiness === "project_bound_no_repo_source") {
5497
+ fixes.add(
5498
+ RUNTIME_MODE === "remote" ? "Add a matching source with context.add_source." : "Ingest local files with index.local_scan_ingest."
5499
+ );
5500
+ }
5501
+ if (args.retrieval_readiness === "project_unverified") {
5502
+ fixes.add("Re-authenticate (valid WHISPER_API_KEY) and rerun fix.");
5503
+ }
5504
+ if (args.retrieval_readiness === "project_repo_source_stale") {
5505
+ fixes.add("Refresh workspace metadata via index.workspace_run.");
5506
+ }
5507
+ return [...fixes];
5508
+ }
4891
5509
  function buildLocalWorkspaceDocuments(rootPath, allowedExts, maxFiles) {
4892
5510
  const files = collectCodeFiles(rootPath, allowedExts, maxFiles);
4893
5511
  const documents = [];
@@ -5023,20 +5641,6 @@ function localHitsToEvidence(workspaceId, hits) {
5023
5641
  )
5024
5642
  );
5025
5643
  }
5026
- function renderLocalWorkspaceContext(args) {
5027
- const lines = args.hits.map((hit, index) => {
5028
- const location = hit.line_end && hit.line_end !== hit.line_start ? `${hit.id}:${hit.line_start}-${hit.line_end}` : `${hit.id}:${hit.line_start}`;
5029
- return `${index + 1}. [${location}, score: ${hit.score.toFixed(2)}, mode: ${hit.search_mode}] ${hit.raw_snippet || hit.snippet || hit.id}`;
5030
- });
5031
- const header = `Found ${args.hits.length} local workspace result(s) (route=${args.route}, workspace=${args.diagnostics.workspace_id}, project=${args.project_ref || "none"}, user=${args.scope.userId}, session=${args.scope.sessionId}):`;
5032
- const warnings = args.warnings.length ? `
5033
-
5034
- [warnings]
5035
- ${args.warnings.join("\n")}` : "";
5036
- return `${header}
5037
-
5038
- ${lines.join("\n\n")}${warnings}`;
5039
- }
5040
5644
  async function runSearchCodeTool(args) {
5041
5645
  const rootPath = canonicalizeWorkspacePath(args.path);
5042
5646
  const allowedExts = args.file_types ? new Set(args.file_types) : CODE_EXTENSIONS;
@@ -5044,12 +5648,21 @@ async function runSearchCodeTool(args) {
5044
5648
  if (files.length === 0) {
5045
5649
  const workspace = await resolveWorkspaceTrust({ path: rootPath });
5046
5650
  const sharedWarnings = [...workspace.warnings];
5651
+ const semanticStatus = "ok";
5652
+ const fallbackMode = "none";
5047
5653
  return {
5048
5654
  tool: "search_code",
5049
5655
  query: args.query,
5050
5656
  path: rootPath,
5051
5657
  results: [],
5052
5658
  count: 0,
5659
+ semantic_status: semanticStatus,
5660
+ fallback_mode: fallbackMode,
5661
+ recommended_fixes: buildRecommendedFixes({
5662
+ retrieval_readiness: workspace.project_ref ? "project_repo_source_ready" : "no_project",
5663
+ semantic_status: semanticStatus,
5664
+ fallback_mode: fallbackMode
5665
+ }),
5053
5666
  warnings: sharedWarnings,
5054
5667
  diagnostics: {
5055
5668
  workspace_id: workspace.workspace_id,
@@ -5078,6 +5691,13 @@ async function runSearchCodeTool(args) {
5078
5691
  path: rootPath,
5079
5692
  results: localRetrieval.results,
5080
5693
  count: localRetrieval.results.length,
5694
+ semantic_status: semanticStatusFromSearchMode(localRetrieval.diagnostics.search_mode),
5695
+ fallback_mode: fallbackModeFromSearchMode(localRetrieval.diagnostics.search_mode),
5696
+ recommended_fixes: buildRecommendedFixes({
5697
+ retrieval_readiness: localRetrieval.workspace.project_ref ? "local_fallback" : "no_project",
5698
+ semantic_status: semanticStatusFromSearchMode(localRetrieval.diagnostics.search_mode),
5699
+ fallback_mode: fallbackModeFromSearchMode(localRetrieval.diagnostics.search_mode)
5700
+ }),
5081
5701
  warnings: localRetrieval.warnings,
5082
5702
  diagnostics: localRetrieval.diagnostics
5083
5703
  };
@@ -5331,7 +5951,8 @@ function renderScopedMcpConfig(project, source, client) {
5331
5951
  env: {
5332
5952
  WHISPER_API_KEY: "wctx_...",
5333
5953
  WHISPER_PROJECT: project,
5334
- WHISPER_SCOPE_SOURCE: source
5954
+ WHISPER_SCOPE_SOURCE: source,
5955
+ WHISPER_MCP_MODE: "auto"
5335
5956
  }
5336
5957
  };
5337
5958
  if (client === "json") {
@@ -5437,6 +6058,7 @@ server.tool(
5437
6058
  },
5438
6059
  async ({ workspace_id, path, mode, max_files }) => {
5439
6060
  try {
6061
+ const startAt = Date.now();
5440
6062
  const identity = resolveWorkspaceIdentity({ path, workspace_id });
5441
6063
  const rootPath = identity.root_path;
5442
6064
  const workspaceId = identity.workspace_id;
@@ -5445,11 +6067,17 @@ server.tool(
5445
6067
  const fileStats = countCodeFiles(rootPath, max_files);
5446
6068
  const coverage = Math.max(0, Math.min(1, fileStats.total / Math.max(1, max_files)));
5447
6069
  const now = (/* @__PURE__ */ new Date()).toISOString();
6070
+ const snapshot = computeWorkspaceSnapshot(rootPath);
5448
6071
  workspace.root_path = rootPath;
5449
6072
  workspace.index_metadata = {
5450
6073
  last_indexed_at: now,
5451
6074
  last_indexed_commit: getGitHead(rootPath),
5452
- coverage: mode === "full" ? 1 : coverage
6075
+ coverage: mode === "full" ? 1 : coverage,
6076
+ indexed_files: fileStats.total,
6077
+ last_auto_refresh_at: now,
6078
+ ...mode === "full" ? { last_full_refresh_at: now } : {},
6079
+ last_snapshot_hash: snapshot.hash,
6080
+ last_snapshot_entries: snapshot.entries
5453
6081
  };
5454
6082
  saveState(state);
5455
6083
  const payload = {
@@ -5458,7 +6086,7 @@ server.tool(
5458
6086
  mode,
5459
6087
  indexed_files: fileStats.total,
5460
6088
  skipped_files: fileStats.skipped,
5461
- duration_ms: 0,
6089
+ duration_ms: Date.now() - startAt,
5462
6090
  warnings: fileStats.total === 0 ? ["No code files discovered for indexing."] : [],
5463
6091
  index_metadata: workspace.index_metadata
5464
6092
  };
@@ -5504,6 +6132,261 @@ server.tool(
5504
6132
  }
5505
6133
  }
5506
6134
  );
6135
+ server.tool(
6136
+ "index.auto_repair",
6137
+ "Diagnose and apply deterministic retrieval/setup repairs. This is the canonical zero-stress repair path.",
6138
+ {
6139
+ path: z.string().optional(),
6140
+ workspace_id: z.string().optional(),
6141
+ project: z.string().optional(),
6142
+ apply: z.boolean().optional().default(true),
6143
+ repair_scope: z.enum(["safe", "full"]).optional().default("safe"),
6144
+ allow_restart_hint: z.boolean().optional().default(true)
6145
+ },
6146
+ async ({ path, workspace_id, project, apply, repair_scope, allow_restart_hint }) => {
6147
+ try {
6148
+ const payload = await runAutoRepairFlow({
6149
+ path,
6150
+ workspace_id,
6151
+ project,
6152
+ apply,
6153
+ repair_scope,
6154
+ allow_restart_hint
6155
+ });
6156
+ return toTextResult(payload);
6157
+ } catch (error) {
6158
+ return primaryToolError(error.message, "auto_repair_failed");
6159
+ }
6160
+ }
6161
+ );
6162
+ server.tool(
6163
+ "fix",
6164
+ "Primary alias for zero-stress setup/retrieval repair. Delegates to index.auto_repair.",
6165
+ {
6166
+ path: z.string().optional(),
6167
+ workspace_id: z.string().optional(),
6168
+ project: z.string().optional(),
6169
+ apply: z.boolean().optional().default(true),
6170
+ repair_scope: z.enum(["safe", "full"]).optional().default("safe"),
6171
+ allow_restart_hint: z.boolean().optional().default(true)
6172
+ },
6173
+ async ({ path, workspace_id, project, apply, repair_scope, allow_restart_hint }) => {
6174
+ try {
6175
+ const payload = await runAutoRepairFlow({
6176
+ path,
6177
+ workspace_id,
6178
+ project,
6179
+ apply,
6180
+ repair_scope,
6181
+ allow_restart_hint
6182
+ });
6183
+ return toTextResult(payload);
6184
+ } catch (error) {
6185
+ return primaryToolError(error.message, "fix_failed");
6186
+ }
6187
+ }
6188
+ );
6189
+ async function runUnifiedContextRetrieval(params) {
6190
+ const preflight = await resolveRepoGroundingPreflight({
6191
+ query: params.question,
6192
+ path: params.path,
6193
+ workspace_id: params.workspace_id,
6194
+ project: params.project,
6195
+ chunk_types: params.chunk_types
6196
+ });
6197
+ const retrievalProfile = resolveMcpRetrievalProfile(params.retrieval_profile);
6198
+ const backgroundRefresh = await runBackgroundWorkspaceRefresh({
6199
+ workspace_id: preflight.trust_state.workspace_id,
6200
+ root_path: preflight.trust_state.root_path,
6201
+ force_full: false
6202
+ });
6203
+ if (shouldAbstainForCodebaseTrust({
6204
+ retrievalProfile,
6205
+ codebaseIntent: preflight.repo_grounded,
6206
+ preflight
6207
+ })) {
6208
+ const abstainPayload = buildCodebaseTrustAbstainPayload({
6209
+ query: params.question,
6210
+ preflight,
6211
+ retrievalProfile
6212
+ });
6213
+ return {
6214
+ ...abstainPayload,
6215
+ semantic_status: "failed",
6216
+ fallback_mode: "none",
6217
+ recommended_fixes: buildRecommendedFixes({
6218
+ retrieval_readiness: preflight.retrieval_readiness,
6219
+ semantic_status: "failed",
6220
+ fallback_mode: "none"
6221
+ }),
6222
+ status_label: "blocked",
6223
+ status_action: "reindex",
6224
+ action_required: true,
6225
+ diagnostics: {
6226
+ retrieval_profile: retrievalProfile,
6227
+ background_refresh: backgroundRefresh
6228
+ }
6229
+ };
6230
+ }
6231
+ const resolvedProject = preflight.trust_state.project_ref || await resolveProjectRef(params.project);
6232
+ const scope = resolveMcpScope({
6233
+ project: resolvedProject || params.project,
6234
+ user_id: params.user_id,
6235
+ session_id: params.session_id,
6236
+ path: preflight.trust_state.root_path
6237
+ });
6238
+ const topK = params.top_k ?? 12;
6239
+ let semanticStatus = "ok";
6240
+ let fallbackMode = "none";
6241
+ let contextText = "";
6242
+ let evidence = [];
6243
+ let warnings = [...preflight.warnings];
6244
+ let retrievalReadiness = preflight.retrieval_readiness;
6245
+ let retrievalRoute = preflight.retrieval_route;
6246
+ let latencyMs = 0;
6247
+ if (preflight.retrieval_route === "local_workspace_fallback") {
6248
+ const localRetrieval = await runLocalWorkspaceRetrieval({
6249
+ query: params.question,
6250
+ path: preflight.trust_state.root_path,
6251
+ project: resolvedProject || params.project,
6252
+ top_k: topK
6253
+ });
6254
+ semanticStatus = semanticStatusFromSearchMode(localRetrieval.diagnostics.search_mode);
6255
+ fallbackMode = fallbackModeFromSearchMode(localRetrieval.diagnostics.search_mode);
6256
+ evidence = localHitsToEvidence(preflight.trust_state.workspace_id, localRetrieval.results);
6257
+ contextText = evidence.map((item) => `[${renderCitation(item)}] ${item.snippet || "Relevant context found."}`).join("\n");
6258
+ warnings = Array.from(/* @__PURE__ */ new Set([...warnings, ...localRetrieval.warnings]));
6259
+ retrievalReadiness = "local_fallback";
6260
+ retrievalRoute = "local_workspace_fallback";
6261
+ } else if (!resolvedProject) {
6262
+ contextText = "";
6263
+ evidence = [];
6264
+ retrievalRoute = "none";
6265
+ } else {
6266
+ const queryResult = await queryWithDegradedFallback({
6267
+ project: resolvedProject,
6268
+ query: params.question,
6269
+ top_k: topK,
6270
+ include_memories: preflight.repo_grounded ? false : params.include_memories,
6271
+ include_graph: params.include_graph,
6272
+ session_id: params.session_id,
6273
+ user_id: scope.userId,
6274
+ source_ids: preflight.repo_grounded ? preflight.matched_source_ids : void 0,
6275
+ retrieval_profile: retrievalProfile,
6276
+ include_parent_content: params.include_parent_content
6277
+ });
6278
+ latencyMs = queryResult.response.meta?.latency_ms || 0;
6279
+ semanticStatus = queryResult.semantic_status || "ok";
6280
+ fallbackMode = queryResult.fallback_mode || "none";
6281
+ const rawResults = preflight.repo_grounded ? filterProjectRepoResults(queryResult.response.results || [], preflight.matched_source_ids) : queryResult.response.results || [];
6282
+ if (preflight.repo_grounded && rawResults.length === 0) {
6283
+ const localRetrieval = await runLocalWorkspaceRetrieval({
6284
+ query: params.question,
6285
+ path: preflight.trust_state.root_path,
6286
+ project: resolvedProject,
6287
+ top_k: topK
6288
+ });
6289
+ semanticStatus = semanticStatusFromSearchMode(localRetrieval.diagnostics.search_mode);
6290
+ fallbackMode = fallbackModeFromSearchMode(localRetrieval.diagnostics.search_mode);
6291
+ evidence = localHitsToEvidence(preflight.trust_state.workspace_id, localRetrieval.results);
6292
+ contextText = evidence.map((item) => `[${renderCitation(item)}] ${item.snippet || "Relevant context found."}`).join("\n");
6293
+ warnings = Array.from(/* @__PURE__ */ new Set([...warnings, ...localRetrieval.warnings]));
6294
+ retrievalReadiness = "local_fallback";
6295
+ retrievalRoute = "local_workspace_fallback";
6296
+ } else {
6297
+ contextText = queryResult.response.context || "";
6298
+ evidence = rawResults.map((r) => toEvidenceRef(r, preflight.trust_state.workspace_id, "semantic"));
6299
+ if (queryResult.degraded_mode) {
6300
+ warnings.push(queryResult.degraded_reason || "Lexical backend fallback active.");
6301
+ }
6302
+ retrievalRoute = preflight.repo_grounded ? "project_repo" : "none";
6303
+ }
6304
+ }
6305
+ recordSemanticAttempt(preflight.trust_state.workspace_id, semanticStatus === "failed");
6306
+ const semanticStats = getWorkspaceSemanticFailureStats(preflight.trust_state.workspace_id);
6307
+ const trustScore = computeTrustScore({
6308
+ retrieval_readiness: retrievalReadiness,
6309
+ workspace_health: preflight.trust_state.health,
6310
+ semantic_failure_rate: semanticStats.rate
6311
+ });
6312
+ persistTrustScore(preflight.trust_state.workspace_id, trustScore);
6313
+ const autoHeal = shouldAutoHeal({
6314
+ retrieval_readiness: retrievalReadiness,
6315
+ trust_score: trustScore,
6316
+ semantic_failure_rate: semanticStats.rate,
6317
+ semantic_attempts: semanticStats.attempts
6318
+ });
6319
+ let autoRepairSummary = null;
6320
+ if (autoHeal) {
6321
+ try {
6322
+ autoRepairSummary = await runAutoRepairFlow({
6323
+ path: preflight.trust_state.root_path,
6324
+ workspace_id: preflight.trust_state.workspace_id,
6325
+ project: resolvedProject || params.project,
6326
+ apply: true,
6327
+ repair_scope: "safe",
6328
+ allow_restart_hint: true
6329
+ });
6330
+ warnings.push("Auto-repair executed in safe mode.");
6331
+ } catch (error) {
6332
+ warnings.push(`Auto-repair failed: ${String(error?.message || error)}`);
6333
+ }
6334
+ }
6335
+ const statusContract = statusContractForReadiness({
6336
+ retrieval_readiness: retrievalReadiness,
6337
+ workspace_health: preflight.trust_state.health,
6338
+ semantic_status: semanticStatus,
6339
+ force_repairing: autoHeal
6340
+ });
6341
+ const recommendedFixes = buildRecommendedFixes({
6342
+ retrieval_readiness: retrievalReadiness,
6343
+ semantic_status: semanticStatus,
6344
+ fallback_mode: fallbackMode
6345
+ });
6346
+ const recommendedNextCalls = sanitizeRecommendedNextCalls([
6347
+ ...preflight.recommended_next_calls,
6348
+ ...semanticStatus === "failed" ? ["fix"] : []
6349
+ ]);
6350
+ const payload = {
6351
+ question: params.question,
6352
+ workspace_id: preflight.trust_state.workspace_id,
6353
+ root_path: preflight.trust_state.root_path,
6354
+ trust_state: preflight.trust_state,
6355
+ retrieval_readiness: retrievalReadiness,
6356
+ retrieval_readiness_label: retrievalReadinessLabel(retrievalReadiness),
6357
+ retrieval_readiness_action: retrievalReadinessAction(retrievalReadiness),
6358
+ retrieval_route: retrievalRoute,
6359
+ context: contextText,
6360
+ evidence,
6361
+ total_results: evidence.length,
6362
+ latency_ms: latencyMs,
6363
+ semantic_status: semanticStatus,
6364
+ fallback_mode: fallbackMode,
6365
+ recommended_fixes: recommendedFixes,
6366
+ status_label: statusContract.status_label,
6367
+ status_action: statusContract.status_action,
6368
+ action_required: statusContract.action_required,
6369
+ warnings: Array.from(new Set(warnings)),
6370
+ recommended_next_calls: recommendedNextCalls,
6371
+ diagnostics: {
6372
+ scope: {
6373
+ project: resolvedProject || params.project || null,
6374
+ user_id: scope.userId,
6375
+ session_id: params.session_id || null
6376
+ },
6377
+ retrieval_profile: retrievalProfile,
6378
+ trust_score: trustScore,
6379
+ semantic_failure_rate: semanticStats.rate,
6380
+ auto_heal_triggered: autoHeal,
6381
+ auto_repair: autoRepairSummary,
6382
+ background_refresh: backgroundRefresh
6383
+ }
6384
+ };
6385
+ if (params.include_alias_warning) {
6386
+ payload.warnings = Array.from(/* @__PURE__ */ new Set([...payload.warnings, "deprecated_alias_use_context.query"]));
6387
+ }
6388
+ return payload;
6389
+ }
5507
6390
  server.tool(
5508
6391
  "context.get_relevant",
5509
6392
  "Default grounded retrieval step for workspace/project questions. Returns ranked evidence with file:line citations and may abstain when workspace/repo trust is not ready.",
@@ -5522,131 +6405,23 @@ server.tool(
5522
6405
  },
5523
6406
  async ({ question, path, workspace_id, project, top_k, include_memories, include_graph, session_id, user_id, include_parent_content, retrieval_profile }) => {
5524
6407
  try {
5525
- const preflight = await resolveRepoGroundingPreflight({ query: question, path, workspace_id, project });
5526
- const retrievalProfile = resolveMcpRetrievalProfile(retrieval_profile);
5527
- if (shouldAbstainForCodebaseTrust({
5528
- retrievalProfile,
5529
- codebaseIntent: preflight.repo_grounded,
5530
- preflight
5531
- })) {
5532
- return toTextResult(buildCodebaseTrustAbstainPayload({
5533
- query: question,
5534
- preflight,
5535
- retrievalProfile
5536
- }));
5537
- }
5538
- if (preflight.retrieval_route === "local_workspace_fallback") {
5539
- const localRetrieval = await runLocalWorkspaceRetrieval({
5540
- query: question,
5541
- path: preflight.trust_state.root_path,
5542
- project: preflight.trust_state.project_ref || project,
5543
- top_k
5544
- });
5545
- const evidence2 = localHitsToEvidence(preflight.trust_state.workspace_id, localRetrieval.results);
5546
- const payload2 = {
5547
- question,
5548
- workspace_id: preflight.trust_state.workspace_id,
5549
- root_path: preflight.trust_state.root_path,
5550
- identity_source: preflight.trust_state.identity_source,
5551
- trust_state: preflight.trust_state,
5552
- retrieval_readiness: preflight.retrieval_readiness,
5553
- retrieval_route: preflight.retrieval_route,
5554
- grounded_to_workspace: true,
5555
- total_results: evidence2.length,
5556
- context: evidence2.map((item) => `[${renderCitation(item)}] ${item.snippet || "Relevant local workspace context found."}`).join("\n"),
5557
- evidence: evidence2,
5558
- used_context_ids: evidence2.map((item) => item.source_id),
5559
- latency_ms: 0,
5560
- warnings: Array.from(/* @__PURE__ */ new Set([...preflight.warnings, ...localRetrieval.warnings])),
5561
- recommended_next_calls: preflight.recommended_next_calls
5562
- };
5563
- return { content: [{ type: "text", text: JSON.stringify(payload2, null, 2) }] };
5564
- }
5565
- if (!preflight.trust_state.project_ref) {
5566
- const payload2 = {
5567
- question,
5568
- workspace_id: preflight.trust_state.workspace_id,
5569
- root_path: preflight.trust_state.root_path,
5570
- identity_source: preflight.trust_state.identity_source,
5571
- trust_state: preflight.trust_state,
5572
- retrieval_readiness: preflight.retrieval_readiness,
5573
- retrieval_route: "none",
5574
- grounded_to_workspace: false,
5575
- total_results: 0,
5576
- context: "",
5577
- evidence: [],
5578
- used_context_ids: [],
5579
- latency_ms: 0,
5580
- warnings: preflight.warnings,
5581
- recommended_next_calls: preflight.recommended_next_calls
5582
- };
5583
- return { content: [{ type: "text", text: JSON.stringify(payload2, null, 2) }] };
5584
- }
5585
- const queryResult = await queryWithDegradedFallback({
5586
- project: preflight.trust_state.project_ref,
5587
- query: question,
6408
+ const payload = await runUnifiedContextRetrieval({
6409
+ question,
6410
+ path,
6411
+ workspace_id,
6412
+ project,
5588
6413
  top_k,
5589
- include_memories: preflight.repo_grounded ? false : include_memories,
6414
+ include_memories,
5590
6415
  include_graph,
5591
6416
  session_id,
5592
6417
  user_id,
5593
- source_ids: preflight.repo_grounded ? preflight.matched_source_ids : void 0,
5594
- retrieval_profile: retrievalProfile,
5595
- include_parent_content
6418
+ include_parent_content,
6419
+ retrieval_profile,
6420
+ include_alias_warning: true
5596
6421
  });
5597
- const response = queryResult.response;
5598
- const rawResults = preflight.repo_grounded ? filterProjectRepoResults(response.results || [], preflight.matched_source_ids) : response.results || [];
5599
- if (preflight.repo_grounded && rawResults.length === 0) {
5600
- const localRetrieval = await runLocalWorkspaceRetrieval({
5601
- query: question,
5602
- path: preflight.trust_state.root_path,
5603
- project: preflight.trust_state.project_ref,
5604
- top_k
5605
- });
5606
- const evidence2 = localHitsToEvidence(preflight.trust_state.workspace_id, localRetrieval.results);
5607
- const payload2 = {
5608
- question,
5609
- workspace_id: preflight.trust_state.workspace_id,
5610
- root_path: preflight.trust_state.root_path,
5611
- identity_source: preflight.trust_state.identity_source,
5612
- trust_state: preflight.trust_state,
5613
- retrieval_readiness: "local_fallback",
5614
- retrieval_route: "local_workspace_fallback",
5615
- grounded_to_workspace: true,
5616
- total_results: evidence2.length,
5617
- context: evidence2.map((item) => `[${renderCitation(item)}] ${item.snippet || "Relevant local workspace context found."}`).join("\n"),
5618
- evidence: evidence2,
5619
- used_context_ids: evidence2.map((item) => item.source_id),
5620
- latency_ms: 0,
5621
- warnings: Array.from(/* @__PURE__ */ new Set([...preflight.warnings, ...localRetrieval.warnings])),
5622
- recommended_next_calls: preflight.recommended_next_calls
5623
- };
5624
- return { content: [{ type: "text", text: JSON.stringify(payload2, null, 2) }] };
5625
- }
5626
- const evidence = rawResults.map((r) => toEvidenceRef(r, preflight.trust_state.workspace_id, "semantic"));
5627
- const payload = {
5628
- question,
5629
- workspace_id: preflight.trust_state.workspace_id,
5630
- root_path: preflight.trust_state.root_path,
5631
- identity_source: preflight.trust_state.identity_source,
5632
- trust_state: preflight.trust_state,
5633
- retrieval_readiness: preflight.retrieval_readiness,
5634
- retrieval_route: preflight.repo_grounded ? "project_repo" : "none",
5635
- grounded_to_workspace: preflight.repo_grounded ? true : preflight.trust_state.grounded_to_workspace,
5636
- total_results: rawResults.length || response.meta?.total || evidence.length,
5637
- context: response.context || "",
5638
- evidence,
5639
- used_context_ids: rawResults.map((r) => String(r.id)),
5640
- latency_ms: response.meta?.latency_ms || 0,
5641
- degraded_mode: queryResult.degraded_mode,
5642
- degraded_reason: queryResult.degraded_reason,
5643
- recommendation: queryResult.recommendation,
5644
- warnings: preflight.warnings,
5645
- recommended_next_calls: preflight.recommended_next_calls
5646
- };
5647
- return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
6422
+ return toTextResult(payload);
5648
6423
  } catch (error) {
5649
- return { content: [{ type: "text", text: `Error: ${error.message}` }] };
6424
+ return primaryToolError(error.message, "context_get_relevant_failed");
5650
6425
  }
5651
6426
  }
5652
6427
  );
@@ -5870,273 +6645,22 @@ server.tool(
5870
6645
  },
5871
6646
  async ({ project, query, path, top_k, chunk_types, include_memories, include_graph, user_id, session_id, max_tokens, include_parent_content, retrieval_profile }) => {
5872
6647
  try {
5873
- const preflight = await resolveRepoGroundingPreflight({ query, path, project, chunk_types });
5874
- const retrievalProfile = resolveMcpRetrievalProfile(retrieval_profile);
5875
- if (shouldAbstainForCodebaseTrust({
5876
- retrievalProfile,
5877
- codebaseIntent: preflight.repo_grounded,
5878
- preflight
5879
- })) {
5880
- return toTextResult(buildCodebaseTrustAbstainPayload({
5881
- query,
5882
- preflight,
5883
- retrievalProfile
5884
- }));
5885
- }
5886
- const resolvedProject = preflight.trust_state.project_ref || await resolveProjectRef(project);
5887
- if (!resolvedProject && !preflight.repo_grounded) {
5888
- return { content: [{ type: "text", text: "Error: No project resolved. Set WHISPER_PROJECT or pass project." }] };
5889
- }
5890
- const scope = resolveMcpScope({ project: resolvedProject, user_id, session_id, path: preflight.trust_state.root_path });
5891
- if (preflight.repo_grounded && preflight.retrieval_route === "local_workspace_fallback") {
5892
- const localRetrieval = await runLocalWorkspaceRetrieval({
5893
- query,
5894
- path: preflight.trust_state.root_path,
5895
- project: resolvedProject || project,
5896
- top_k
5897
- });
5898
- if (localRetrieval.results.length > 0) {
5899
- return {
5900
- content: [{
5901
- type: "text",
5902
- text: renderLocalWorkspaceContext({
5903
- query,
5904
- project_ref: resolvedProject || null,
5905
- scope,
5906
- route: "local_workspace_fallback",
5907
- hits: localRetrieval.results,
5908
- diagnostics: localRetrieval.diagnostics,
5909
- warnings: Array.from(/* @__PURE__ */ new Set([...preflight.warnings, ...localRetrieval.warnings]))
5910
- })
5911
- }]
5912
- };
5913
- }
5914
- }
5915
- if (preflight.repo_grounded && resolvedProject) {
5916
- const queryResult2 = await queryWithDegradedFallback({
5917
- project: resolvedProject,
5918
- query,
5919
- top_k,
5920
- include_memories: false,
5921
- include_graph,
5922
- user_id: user_id || scope.userId,
5923
- session_id: session_id || scope.sessionId,
5924
- source_ids: preflight.matched_source_ids,
5925
- retrieval_profile: retrievalProfile,
5926
- include_parent_content
5927
- });
5928
- const repoResults = filterProjectRepoResults(queryResult2.response.results || [], preflight.matched_source_ids);
5929
- if (repoResults.length > 0) {
5930
- const scopedResponse = { ...queryResult2.response, results: repoResults };
5931
- const header2 = `Found ${repoResults.length} repo-grounded result(s) (${scopedResponse.meta.latency_ms}ms${scopedResponse.meta.cache_hit ? ", cached" : ""}, route=project_repo, workspace=${preflight.trust_state.workspace_id}, project=${resolvedProject}, user=${scope.userId}, session=${scope.sessionId}):
5932
-
5933
- `;
5934
- const suffix2 = [
5935
- `[diagnostics] identity_source=${preflight.trust_state.identity_source} retrieval_readiness=${preflight.retrieval_readiness} retrieval_route=project_repo`,
5936
- queryResult2.degraded_mode ? `[degraded_mode=true] ${queryResult2.degraded_reason}
5937
- Recommendation: ${queryResult2.recommendation}` : "",
5938
- preflight.warnings.length ? `[warnings]
5939
- ${preflight.warnings.join("\n")}` : ""
5940
- ].filter(Boolean).join("\n\n");
5941
- return { content: [{ type: "text", text: `${header2}${scopedResponse.context}${suffix2 ? `
5942
-
5943
- ${suffix2}` : ""}` }] };
5944
- }
5945
- }
5946
- if (preflight.repo_grounded) {
5947
- if (resolvedProject && include_memories !== false) {
5948
- const memoryRescue = await runContextQueryMemoryRescue({
5949
- project: resolvedProject,
5950
- query,
5951
- user_id: user_id ? scope.userId : void 0,
5952
- session_id: session_id ? scope.sessionId : void 0,
5953
- top_k
5954
- });
5955
- if (memoryRescue.results.length && memoryRescue.rescue_mode) {
5956
- return {
5957
- content: [{
5958
- type: "text",
5959
- text: `${renderContextQueryMemoryRescue({
5960
- project: resolvedProject,
5961
- query,
5962
- scope,
5963
- results: memoryRescue.results,
5964
- rescue_mode: memoryRescue.rescue_mode
5965
- })}
5966
-
5967
- [diagnostics]
5968
- retrieval_route=memory_only retrieval_readiness=${preflight.retrieval_readiness} workspace=${preflight.trust_state.workspace_id}`
5969
- }]
5970
- };
5971
- }
5972
- }
5973
- return {
5974
- content: [{
5975
- type: "text",
5976
- text: `No relevant repo-grounded context found.
5977
-
5978
- [diagnostics]
5979
- workspace=${preflight.trust_state.workspace_id} identity_source=${preflight.trust_state.identity_source} retrieval_readiness=${preflight.retrieval_readiness} retrieval_route=${preflight.retrieval_route}`
5980
- }]
5981
- };
5982
- }
5983
- const automaticMode = !preflight.repo_grounded && include_memories !== false && include_graph !== true && !(chunk_types && chunk_types.length > 0) && max_tokens === void 0 && runtimeClient;
5984
- if (automaticMode) {
5985
- try {
5986
- const prepared = await prepareAutomaticQuery({
5987
- project: resolvedProject,
5988
- query,
5989
- top_k,
5990
- user_id,
5991
- session_id,
5992
- path: preflight.trust_state.root_path
5993
- });
5994
- if (!prepared.items.length) {
5995
- const memoryRescue = include_memories !== false ? await runContextQueryMemoryRescue({
5996
- project: resolvedProject,
5997
- query,
5998
- user_id: user_id ? scope.userId : void 0,
5999
- session_id: session_id ? scope.sessionId : void 0,
6000
- top_k
6001
- }) : { results: [], rescue_mode: null };
6002
- if (memoryRescue.results.length && memoryRescue.rescue_mode) {
6003
- return {
6004
- content: [{
6005
- type: "text",
6006
- text: renderContextQueryMemoryRescue({
6007
- project: resolvedProject,
6008
- query,
6009
- scope,
6010
- results: memoryRescue.results,
6011
- rescue_mode: memoryRescue.rescue_mode
6012
- })
6013
- }]
6014
- };
6015
- }
6016
- return { content: [{ type: "text", text: "No relevant context found." }] };
6017
- }
6018
- const warnings = prepared.retrieval.warnings.length ? `
6019
-
6020
- [automatic_runtime]
6021
- ${prepared.retrieval.warnings.join("\n")}` : "";
6022
- const diagnostics = [
6023
- `focused_scope=${prepared.retrieval.focusedScopeApplied}`,
6024
- `fallback_used=${prepared.retrieval.fallbackUsed}`,
6025
- `deduped=${prepared.retrieval.dedupedCount}`,
6026
- `dropped_below_floor=${prepared.retrieval.droppedBelowFloor}`
6027
- ].join(" ");
6028
- const scopeLabel = `project=${prepared.scope.project} user=${prepared.scope.userId} session=${prepared.scope.sessionId}`;
6029
- return {
6030
- content: [{
6031
- type: "text",
6032
- text: `Found ${prepared.items.length} runtime-ranked items (${prepared.retrieval.durationMs}ms, ${scopeLabel}, ${diagnostics}):
6033
-
6034
- ${prepared.context}${warnings}`
6035
- }]
6036
- };
6037
- } catch (error) {
6038
- const automaticWarning = `Automatic runtime unavailable: ${error.message}. Falling back to broad/manual query path.`;
6039
- const queryResult2 = await queryWithDegradedFallback({
6040
- project: resolvedProject,
6041
- query,
6042
- top_k,
6043
- include_memories: include_memories === true,
6044
- include_graph,
6045
- user_id: user_id || scope.userId,
6046
- session_id: session_id || scope.sessionId,
6047
- retrieval_profile: retrievalProfile,
6048
- include_parent_content
6049
- });
6050
- const response2 = queryResult2.response;
6051
- if (response2.results.length === 0) {
6052
- const memoryRescue = include_memories !== false ? await runContextQueryMemoryRescue({
6053
- project: resolvedProject,
6054
- query,
6055
- user_id: user_id ? scope.userId : void 0,
6056
- session_id: session_id ? scope.sessionId : void 0,
6057
- top_k
6058
- }) : { results: [], rescue_mode: null };
6059
- if (memoryRescue.results.length && memoryRescue.rescue_mode) {
6060
- return {
6061
- content: [{
6062
- type: "text",
6063
- text: `${renderContextQueryMemoryRescue({
6064
- project: resolvedProject,
6065
- query,
6066
- scope,
6067
- results: memoryRescue.results,
6068
- rescue_mode: memoryRescue.rescue_mode
6069
- })}
6070
-
6071
- [automatic_runtime]
6072
- ${automaticWarning}`
6073
- }]
6074
- };
6075
- }
6076
- return { content: [{ type: "text", text: `No relevant context found.
6077
-
6078
- [automatic_runtime]
6079
- ${automaticWarning}` }] };
6080
- }
6081
- const header2 = `Found ${response2.meta.total} results (${response2.meta.latency_ms}ms${response2.meta.cache_hit ? ", cached" : ""}, project=${resolvedProject}, user=${scope.userId}, session=${scope.sessionId}):
6082
-
6083
- `;
6084
- const suffix2 = queryResult2.degraded_mode ? `
6085
-
6086
- [degraded_mode=true] ${queryResult2.degraded_reason}
6087
- Recommendation: ${queryResult2.recommendation}` : "";
6088
- return { content: [{ type: "text", text: `${header2}${response2.context}
6089
-
6090
- [automatic_runtime]
6091
- ${automaticWarning}${suffix2}` }] };
6092
- }
6093
- }
6094
- const queryResult = await queryWithDegradedFallback({
6095
- project: resolvedProject,
6096
- query,
6648
+ const payload = await runUnifiedContextRetrieval({
6649
+ question: query,
6650
+ path,
6651
+ project,
6097
6652
  top_k,
6098
- include_memories: include_memories === true,
6653
+ include_memories,
6099
6654
  include_graph,
6100
- user_id: user_id || scope.userId,
6101
- session_id: session_id || scope.sessionId,
6102
- retrieval_profile: retrievalProfile,
6103
- include_parent_content
6655
+ user_id,
6656
+ session_id,
6657
+ include_parent_content,
6658
+ retrieval_profile,
6659
+ chunk_types
6104
6660
  });
6105
- const response = queryResult.response;
6106
- if (response.results.length === 0) {
6107
- const memoryRescue = include_memories !== false ? await runContextQueryMemoryRescue({
6108
- project: resolvedProject,
6109
- query,
6110
- user_id: user_id ? scope.userId : void 0,
6111
- session_id: session_id ? scope.sessionId : void 0,
6112
- top_k
6113
- }) : { results: [], rescue_mode: null };
6114
- if (memoryRescue.results.length && memoryRescue.rescue_mode) {
6115
- return {
6116
- content: [{
6117
- type: "text",
6118
- text: renderContextQueryMemoryRescue({
6119
- project: resolvedProject,
6120
- query,
6121
- scope,
6122
- results: memoryRescue.results,
6123
- rescue_mode: memoryRescue.rescue_mode
6124
- })
6125
- }]
6126
- };
6127
- }
6128
- return { content: [{ type: "text", text: "No relevant context found." }] };
6129
- }
6130
- const header = `Found ${response.meta.total} results (${response.meta.latency_ms}ms${response.meta.cache_hit ? ", cached" : ""}, project=${resolvedProject}, user=${scope.userId}, session=${scope.sessionId}):
6131
-
6132
- `;
6133
- const suffix = queryResult.degraded_mode ? `
6134
-
6135
- [degraded_mode=true] ${queryResult.degraded_reason}
6136
- Recommendation: ${queryResult.recommendation}` : "";
6137
- return { content: [{ type: "text", text: header + response.context + suffix }] };
6661
+ return toTextResult(payload);
6138
6662
  } catch (error) {
6139
- return { content: [{ type: "text", text: `Error: ${error.message}` }] };
6663
+ return primaryToolError(error.message, "context_query_failed");
6140
6664
  }
6141
6665
  }
6142
6666
  );
@@ -6168,11 +6692,24 @@ server.tool(
6168
6692
  const jobId = result?.job_id;
6169
6693
  const mode = result?.mode;
6170
6694
  const semanticStatus = result?.semantic_status;
6171
- const typeLabel = memory_type || "factual";
6172
- const text = mode === "async" || jobId ? `Memory queued (job_id: ${jobId || result.id}, type: ${typeLabel}, user=${scope.userId}, session=${scope.sessionId}).` : `Memory stored (id: ${memoryId}, type: ${typeLabel}, user=${scope.userId}, session=${scope.sessionId}${semanticStatus ? `, semantic_status: ${semanticStatus}` : ""}).`;
6173
- return { content: [{ type: "text", text }] };
6695
+ return primaryToolSuccess({
6696
+ tool: "memory.add",
6697
+ memory_id: memoryId || null,
6698
+ job_id: jobId || null,
6699
+ mode: mode || null,
6700
+ semantic_status: semanticStatus || null,
6701
+ memory_type: memory_type || "factual",
6702
+ queued: mode === "async" || Boolean(jobId),
6703
+ diagnostics: {
6704
+ scope: {
6705
+ project: scope.project || null,
6706
+ user_id: scope.userId,
6707
+ session_id: scope.sessionId || null
6708
+ }
6709
+ }
6710
+ });
6174
6711
  } catch (error) {
6175
- return { content: [{ type: "text", text: `Error: ${error.message}` }] };
6712
+ return primaryToolError(error.message);
6176
6713
  }
6177
6714
  }
6178
6715
  );
@@ -6214,7 +6751,14 @@ server.tool(
6214
6751
  user_id: scope.userId,
6215
6752
  session_id: scope.sessionId,
6216
6753
  results: normalizedResults,
6217
- count: normalizedResults.length
6754
+ count: normalizedResults.length,
6755
+ diagnostics: {
6756
+ scope: {
6757
+ project: scope.project || null,
6758
+ user_id: scope.userId,
6759
+ session_id: scope.sessionId || null
6760
+ }
6761
+ }
6218
6762
  });
6219
6763
  } catch (error) {
6220
6764
  return primaryToolError(error.message);
@@ -6478,11 +7022,12 @@ server.tool(
6478
7022
  },
6479
7023
  async ({ project, query, user_id, session_id, question_date, memory_types, top_k, include_relations }) => {
6480
7024
  try {
7025
+ const scope = resolveMcpScope({ project, user_id, session_id });
6481
7026
  const results = await whisper.searchMemoriesSOTA({
6482
- project,
7027
+ project: scope.project,
6483
7028
  query,
6484
- user_id,
6485
- session_id,
7029
+ user_id: scope.userId,
7030
+ session_id: scope.sessionId,
6486
7031
  question_date,
6487
7032
  memory_types,
6488
7033
  top_k,
@@ -6492,10 +7037,19 @@ server.tool(
6492
7037
  return primaryToolSuccess({
6493
7038
  tool: "memory.search_sota",
6494
7039
  query,
7040
+ user_id: scope.userId,
7041
+ session_id: scope.sessionId || null,
6495
7042
  question_date: question_date || null,
6496
7043
  include_relations,
6497
7044
  results: normalizedResults,
6498
- count: normalizedResults.length
7045
+ count: normalizedResults.length,
7046
+ diagnostics: {
7047
+ scope: {
7048
+ project: scope.project || null,
7049
+ user_id: scope.userId,
7050
+ session_id: scope.sessionId || null
7051
+ }
7052
+ }
6499
7053
  });
6500
7054
  } catch (error) {
6501
7055
  return primaryToolError(error.message);
@@ -7531,7 +8085,10 @@ server.tool(
7531
8085
  context: response.context,
7532
8086
  results: response.results,
7533
8087
  degradedMode: queryResult.degraded_mode,
7534
- degradedReason: queryResult.degraded_reason
8088
+ degradedReason: queryResult.degraded_reason,
8089
+ semanticStatus: queryResult.semantic_status,
8090
+ fallbackMode: queryResult.fallback_mode,
8091
+ recommendedFixes: queryResult.recommended_fixes || []
7535
8092
  }));
7536
8093
  } catch (error) {
7537
8094
  return primaryToolError(error.message);
@@ -7795,7 +8352,16 @@ server.tool(
7795
8352
  },
7796
8353
  async ({ project, content, memory_type, user_id, session_id, agent_id, importance }) => {
7797
8354
  try {
7798
- const result = await whisper.addMemory({ project, content, memory_type, user_id, session_id, agent_id, importance });
8355
+ const scope = resolveMcpScope({ project, user_id, session_id });
8356
+ const result = await whisper.addMemory({
8357
+ project: scope.project,
8358
+ content,
8359
+ memory_type,
8360
+ user_id: scope.userId,
8361
+ session_id: scope.sessionId,
8362
+ agent_id,
8363
+ importance
8364
+ });
7799
8365
  const memoryId = result?.memory_id || (result.mode === "sync" ? result.id : null);
7800
8366
  const jobId = result?.job_id || (result.mode === "async" ? result.id : null);
7801
8367
  return primaryToolSuccess({
@@ -7806,7 +8372,14 @@ server.tool(
7806
8372
  mode: result?.mode || null,
7807
8373
  memory_type,
7808
8374
  stored: result.success === true,
7809
- queued: result?.mode === "async" || Boolean(jobId)
8375
+ queued: result?.mode === "async" || Boolean(jobId),
8376
+ diagnostics: {
8377
+ scope: {
8378
+ project: scope.project || null,
8379
+ user_id: scope.userId,
8380
+ session_id: scope.sessionId || null
8381
+ }
8382
+ }
7810
8383
  });
7811
8384
  } catch (error) {
7812
8385
  return primaryToolError(error.message);
@@ -7842,18 +8415,31 @@ server.tool(
7842
8415
  timestamp
7843
8416
  });
7844
8417
  const resolvedProject = await resolveProjectRef(project);
7845
- const result = await ingestSessionWithSyncFallback({
7846
- project: resolvedProject,
7847
- session_id,
8418
+ const scope = resolveMcpScope({
8419
+ project: resolvedProject || project,
7848
8420
  user_id,
8421
+ session_id
8422
+ });
8423
+ const result = await ingestSessionWithSyncFallback({
8424
+ project: scope.project,
8425
+ session_id: scope.sessionId || session_id,
8426
+ user_id: scope.userId,
7849
8427
  messages: normalizedMessages
7850
8428
  });
7851
8429
  return primaryToolSuccess({
7852
8430
  tool: "record",
7853
- session_id,
8431
+ session_id: scope.sessionId || session_id,
8432
+ user_id: scope.userId,
7854
8433
  messages_recorded: normalizedMessages.length,
7855
8434
  memories_created: result.memories_created,
7856
- relations_created: result.relations_created
8435
+ relations_created: result.relations_created,
8436
+ diagnostics: {
8437
+ scope: {
8438
+ project: scope.project || null,
8439
+ user_id: scope.userId,
8440
+ session_id: scope.sessionId || session_id
8441
+ }
8442
+ }
7857
8443
  });
7858
8444
  } catch (error) {
7859
8445
  return primaryToolError(error.message);
@@ -7984,6 +8570,14 @@ async function main() {
7984
8570
  console.error("Error: WHISPER_API_KEY environment variable is required");
7985
8571
  process.exit(1);
7986
8572
  }
8573
+ const startupDiagnostics = {
8574
+ runtime_mode: RUNTIME_MODE,
8575
+ local_ingest_enabled: RUNTIME_MODE !== "remote",
8576
+ startup_status: RUNTIME_MODE === "remote" ? "degraded_auto_repairing" : "healthy",
8577
+ startup_action: RUNTIME_MODE === "remote" ? "restart_client" : "none",
8578
+ warning: RUNTIME_MODE === "remote" ? "Local ingest is disabled in remote mode. Set WHISPER_MCP_MODE=auto and restart MCP to enable local repair paths." : null
8579
+ };
8580
+ console.error(`[whisper-context-mcp] startup_diagnostics=${JSON.stringify(startupDiagnostics)}`);
7987
8581
  console.error("[whisper-context-mcp] Breaking change: canonical namespaced tool names are active. Run with --print-tool-map for migration mapping.");
7988
8582
  const transport = new StdioServerTransport();
7989
8583
  await server.connect(transport);
@@ -7996,20 +8590,35 @@ export {
7996
8590
  RECOMMENDED_NEXT_CALL_ALLOWLIST,
7997
8591
  RETRIEVAL_READINESS_VALUES,
7998
8592
  RETRIEVAL_ROUTE_VALUES,
8593
+ STATUS_ACTION_VALUES,
8594
+ STATUS_LABEL_VALUES,
7999
8595
  WORKSPACE_HEALTH_VALUES,
8000
8596
  canonicalizeWorkspacePath,
8001
8597
  chooseWorkspaceProjectSource,
8002
8598
  classifyProjectRepoReadiness,
8003
8599
  classifyRepoGroundedQuery,
8004
8600
  classifyWorkspaceHealth,
8601
+ computeSnapshotChangedFiles,
8602
+ computeTrustScore,
8005
8603
  createMcpServer,
8006
8604
  createWhisperMcpClient,
8007
8605
  createWhisperMcpRuntimeClient,
8606
+ defaultMcpUserId,
8008
8607
  extractSignature,
8608
+ fallbackModeFromSearchMode,
8609
+ isStrictlyBetterReadiness,
8610
+ recommendedNextCallsAreAcyclic,
8611
+ recommendedNextCallsForRootCause,
8009
8612
  renderScopedMcpConfig,
8010
8613
  resolveForgetQueryCandidates,
8614
+ resolveMcpScope,
8011
8615
  resolveWorkspaceIdentity,
8616
+ retrievalReadinessRank,
8012
8617
  runSearchCodeSearch,
8013
8618
  sanitizeRecommendedNextCalls,
8014
- shouldAbstainForCodebaseTrust
8619
+ semanticStatusFromSearchMode,
8620
+ shouldAbstainForCodebaseTrust,
8621
+ shouldTriggerBackgroundRefresh,
8622
+ statusActionForRootCause,
8623
+ statusContractForReadiness
8015
8624
  };