@usewhisper/mcp-server 2.13.0 → 2.15.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 +1435 -537
  3. package/package.json +2 -2
package/dist/server.js CHANGED
@@ -7,7 +7,7 @@ import { z } from "zod";
7
7
  import { execSync, spawnSync } from "child_process";
8
8
  import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync, appendFileSync } from "fs";
9
9
  import { join, relative, extname, normalize as normalizePath, resolve as resolvePath } from "path";
10
- import { homedir } from "os";
10
+ import { homedir, userInfo } from "os";
11
11
  import { createHash, randomUUID } from "crypto";
12
12
 
13
13
  // ../src/sdk/core/telemetry.ts
@@ -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,110 @@ async function ingestSessionWithSyncFallback(params) {
4498
4711
  return whisper.ingestSession(params);
4499
4712
  }
4500
4713
  }
4501
- function defaultMcpUserId() {
4714
+ function apiKeyFingerprint(apiKey) {
4715
+ const value = String(apiKey || "").trim();
4716
+ if (!value) return "anon";
4717
+ return createHash("sha256").update(value).digest("hex").slice(0, 12);
4718
+ }
4719
+ function defaultMcpUserId(params) {
4502
4720
  const explicit = process.env.WHISPER_USER_ID?.trim();
4503
4721
  if (explicit) return explicit;
4504
- const seed = `${process.cwd()}|${DEFAULT_PROJECT || "default"}|${API_KEY.slice(0, 12) || "anon"}`;
4722
+ let osUsername = process.env.USER?.trim() || process.env.USERNAME?.trim();
4723
+ if (!osUsername) {
4724
+ try {
4725
+ osUsername = userInfo().username?.trim();
4726
+ } catch {
4727
+ osUsername = "";
4728
+ }
4729
+ }
4730
+ if (!osUsername) {
4731
+ throw new Error("Unable to derive user identity. Set WHISPER_USER_ID or pass user_id.");
4732
+ }
4733
+ const workspacePath = params?.workspacePath || canonicalizeWorkspacePath(process.cwd());
4734
+ const projectRef = params?.project || DEFAULT_PROJECT || "default";
4735
+ const seed = `${workspacePath}|${projectRef}|${apiKeyFingerprint(API_KEY)}|${osUsername.toLowerCase()}`;
4505
4736
  return `mcp-user-${createHash("sha256").update(seed).digest("hex").slice(0, 12)}`;
4506
4737
  }
4507
- function resolveMcpScope(params) {
4738
+ function resolveMcpScope(params, options) {
4739
+ const workspacePath = canonicalizeWorkspacePath(params?.path || process.env.WHISPER_WORKSPACE_PATH || process.cwd());
4740
+ const explicitSession = params?.session_id?.trim();
4508
4741
  return {
4509
4742
  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())
4743
+ userId: params?.user_id?.trim() || defaultMcpUserId({
4744
+ project: params?.project,
4745
+ workspacePath
4746
+ }),
4747
+ sessionId: explicitSession || (options?.include_default_session ? cachedMcpSessionId : void 0),
4748
+ workspacePath
4513
4749
  };
4514
4750
  }
4515
- async function prepareAutomaticQuery(params) {
4516
- if (!runtimeClient) {
4517
- throw new Error("Whisper runtime client unavailable.");
4751
+ function resolveMemoryScope(params) {
4752
+ const base = resolveMcpScope({
4753
+ project: params?.project,
4754
+ user_id: params?.user_id,
4755
+ session_id: params?.session_id,
4756
+ path: params?.path
4757
+ });
4758
+ const scopeMode = params?.scope === "shared_project" ? "shared_project" : "personal";
4759
+ if (scopeMode === "shared_project") {
4760
+ if (!base.project && !DEFAULT_PROJECT) {
4761
+ throw new Error("shared_project scope requires a project. Set WHISPER_PROJECT or pass project.");
4762
+ }
4763
+ const projectRef = base.project || DEFAULT_PROJECT || "default";
4764
+ return {
4765
+ ...base,
4766
+ userId: `shared-project:${projectRef}`,
4767
+ sessionId: void 0,
4768
+ actorUserId: base.userId,
4769
+ actorSessionId: base.sessionId,
4770
+ scopeMode,
4771
+ sharedProjectRef: projectRef
4772
+ };
4518
4773
  }
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);
4774
+ return {
4775
+ ...base,
4776
+ actorUserId: base.userId,
4777
+ actorSessionId: base.sessionId,
4778
+ scopeMode,
4779
+ sharedProjectRef: null
4780
+ };
4781
+ }
4782
+ function sharedProjectMemoryUserId(projectRef) {
4783
+ return `shared-project:${projectRef}`;
4784
+ }
4785
+ function buildMemoryScopeMetadata(args) {
4786
+ const { memoryScope } = args;
4787
+ const metadata = {
4788
+ scope: memoryScope.scopeMode,
4789
+ ...memoryScope.sharedProjectRef ? { shared_project_ref: memoryScope.sharedProjectRef } : {}
4790
+ };
4791
+ if (memoryScope.scopeMode === "shared_project") {
4792
+ metadata.provenance = {
4793
+ writer_user_id: memoryScope.actorUserId,
4794
+ writer_session_id: memoryScope.actorSessionId || null,
4795
+ writer_agent_id: args.agent_id || process.env.WHISPER_AGENT_ID || null,
4796
+ written_at: (/* @__PURE__ */ new Date()).toISOString()
4797
+ };
4538
4798
  }
4539
- return runtime.beforeTurn({
4540
- userMessage: params.query
4541
- });
4799
+ return metadata;
4800
+ }
4801
+ function isMemoryInScope(args) {
4802
+ const memory = args.memory;
4803
+ if (!memory || typeof memory !== "object") return false;
4804
+ const memoryUserId = String(memory.userId || memory.user_id || "").trim();
4805
+ if (args.memoryScope.scopeMode === "shared_project") {
4806
+ const metadata = memory.metadata && typeof memory.metadata === "object" ? memory.metadata : {};
4807
+ const metaScope = String(metadata.scope || "").trim();
4808
+ const metaProjectRef = String(metadata.shared_project_ref || "").trim();
4809
+ return memoryUserId === args.memoryScope.userId || metaScope === "shared_project" && metaProjectRef === (args.memoryScope.sharedProjectRef || "");
4810
+ }
4811
+ return memoryUserId === args.memoryScope.userId;
4542
4812
  }
4543
4813
  function noteAutomaticSourceActivity(params) {
4544
4814
  if (!runtimeClient) return;
4545
4815
  const sourceIds = [...new Set((params.sourceIds || []).map((value) => String(value || "").trim()).filter(Boolean))];
4546
4816
  if (sourceIds.length === 0) return;
4547
- const scope = resolveMcpScope(params);
4817
+ const scope = resolveMcpScope(params, { include_default_session: true });
4548
4818
  const key = [
4549
4819
  params.project || DEFAULT_PROJECT || "",
4550
4820
  scope.userId,
@@ -4576,7 +4846,7 @@ function buildAbstain(args) {
4576
4846
  warnings: args.warnings || [],
4577
4847
  trust_state: args.trust_state,
4578
4848
  recommended_next_calls: sanitizeRecommendedNextCalls(
4579
- args.recommended_next_calls || ["index.workspace_status", "index.workspace_run", "grep", "context.get_relevant"]
4849
+ args.recommended_next_calls || ["index.workspace_status", "index.workspace_run", "fix", "context.query"]
4580
4850
  ),
4581
4851
  diagnostics: {
4582
4852
  claims_evaluated: args.claims_evaluated,
@@ -4631,6 +4901,415 @@ function countCodeFiles(searchPath, maxFiles = 5e3) {
4631
4901
  walk(searchPath);
4632
4902
  return { total, skipped };
4633
4903
  }
4904
+ function computeWorkspaceSnapshot(searchPath, maxFiles = 250) {
4905
+ const files = collectCodeFiles(searchPath, CODE_EXTENSIONS, maxFiles);
4906
+ const entries = files.slice(0, maxFiles).map((filePath) => {
4907
+ try {
4908
+ const st = statSync(filePath);
4909
+ return `${relative(searchPath, filePath)}:${st.mtimeMs}:${st.size}`;
4910
+ } catch {
4911
+ return "";
4912
+ }
4913
+ }).filter(Boolean);
4914
+ const sample = entries.join("|");
4915
+ return {
4916
+ hash: createHash("sha256").update(sample || searchPath).digest("hex").slice(0, 24),
4917
+ entries
4918
+ };
4919
+ }
4920
+ function computeSnapshotChangedFiles(previousEntries, nextEntries) {
4921
+ if (!previousEntries || previousEntries.length === 0) return nextEntries.length;
4922
+ const previousByPath = /* @__PURE__ */ new Map();
4923
+ for (const entry of previousEntries) {
4924
+ const idx = entry.indexOf(":");
4925
+ if (idx <= 0) continue;
4926
+ previousByPath.set(entry.slice(0, idx), entry.slice(idx + 1));
4927
+ }
4928
+ let changed = 0;
4929
+ const seen = /* @__PURE__ */ new Set();
4930
+ for (const entry of nextEntries) {
4931
+ const idx = entry.indexOf(":");
4932
+ if (idx <= 0) continue;
4933
+ const relPath = entry.slice(0, idx);
4934
+ const fingerprint = entry.slice(idx + 1);
4935
+ seen.add(relPath);
4936
+ if (previousByPath.get(relPath) !== fingerprint) changed += 1;
4937
+ }
4938
+ for (const relPath of previousByPath.keys()) {
4939
+ if (!seen.has(relPath)) changed += 1;
4940
+ }
4941
+ return changed;
4942
+ }
4943
+ function shouldTriggerBackgroundRefresh(args) {
4944
+ const nowMs = args.now_ms ?? Date.now();
4945
+ const lastAutoRefreshAt = args.last_auto_refresh_at ? new Date(args.last_auto_refresh_at).getTime() : 0;
4946
+ const debounceMs = 10 * 60 * 1e3;
4947
+ const dueForDebounce = !lastAutoRefreshAt || Number.isNaN(lastAutoRefreshAt) || nowMs - lastAutoRefreshAt >= debounceMs;
4948
+ if (!dueForDebounce) {
4949
+ return { should_refresh: false, trigger_reason: null };
4950
+ }
4951
+ if (args.last_indexed_commit && args.current_head && args.last_indexed_commit !== args.current_head) {
4952
+ return { should_refresh: true, trigger_reason: "head_changed" };
4953
+ }
4954
+ if (args.changed_files >= 20) {
4955
+ return { should_refresh: true, trigger_reason: "delta_files_threshold" };
4956
+ }
4957
+ const ratio = args.indexed_files > 0 ? args.changed_files / args.indexed_files : 0;
4958
+ if (ratio >= 0.02) {
4959
+ return { should_refresh: true, trigger_reason: "delta_ratio_threshold" };
4960
+ }
4961
+ return { should_refresh: false, trigger_reason: null };
4962
+ }
4963
+ async function runBackgroundWorkspaceRefresh(args) {
4964
+ const state = loadState();
4965
+ const workspace = getWorkspaceState(state, args.workspace_id);
4966
+ if (!workspace.index_metadata) workspace.index_metadata = {};
4967
+ const fileStats = countCodeFiles(args.root_path, args.max_files || 1500);
4968
+ const currentHead = getGitHead(args.root_path) || null;
4969
+ const currentSnapshot = computeWorkspaceSnapshot(args.root_path);
4970
+ const gitPendingCount = getGitPendingCount(args.root_path);
4971
+ const changedFiles = gitPendingCount == null ? computeSnapshotChangedFiles(workspace.index_metadata.last_snapshot_entries, currentSnapshot.entries) : gitPendingCount;
4972
+ const lastIndexedCommit = workspace.index_metadata.last_indexed_commit || null;
4973
+ const refreshDecision = shouldTriggerBackgroundRefresh({
4974
+ current_head: currentHead,
4975
+ last_indexed_commit: lastIndexedCommit,
4976
+ changed_files: changedFiles,
4977
+ indexed_files: parsePositiveNumber(workspace.index_metadata.indexed_files, fileStats.total || 1),
4978
+ last_auto_refresh_at: workspace.index_metadata.last_auto_refresh_at || null
4979
+ });
4980
+ const forceFullRequested = args.force_full === true;
4981
+ if (!refreshDecision.should_refresh && !forceFullRequested) {
4982
+ return { refreshed: false, mode: null, reason: null };
4983
+ }
4984
+ const mode = forceFullRequested ? "full" : "incremental";
4985
+ workspace.root_path = args.root_path;
4986
+ workspace.index_metadata.last_indexed_at = (/* @__PURE__ */ new Date()).toISOString();
4987
+ workspace.index_metadata.last_auto_refresh_at = (/* @__PURE__ */ new Date()).toISOString();
4988
+ workspace.index_metadata.last_indexed_commit = currentHead || void 0;
4989
+ workspace.index_metadata.coverage = mode === "full" ? 1 : Math.max(0, Math.min(1, fileStats.total / Math.max(1, args.max_files || 1500)));
4990
+ workspace.index_metadata.indexed_files = fileStats.total;
4991
+ workspace.index_metadata.last_snapshot_hash = currentSnapshot.hash;
4992
+ workspace.index_metadata.last_snapshot_entries = currentSnapshot.entries;
4993
+ if (mode === "full") workspace.index_metadata.last_full_refresh_at = (/* @__PURE__ */ new Date()).toISOString();
4994
+ saveState(state);
4995
+ return {
4996
+ refreshed: true,
4997
+ mode,
4998
+ reason: refreshDecision.trigger_reason || (forceFullRequested ? "manual_full_override" : mode === "full" ? "full_refresh_due" : "manual")
4999
+ };
5000
+ }
5001
+ function buildSourceRepairHint(args) {
5002
+ const remote = parseGitHubRemote(getGitRemoteUrl(args.root_path) || null);
5003
+ if (remote) {
5004
+ const branch = getGitBranch(args.root_path) || "main";
5005
+ return {
5006
+ command: `context.add_source --project ${args.project_ref || "<project>"} --type github --owner ${remote.owner} --repo ${remote.repo} --branch ${branch}`,
5007
+ message: "Attach the matching GitHub source, then rerun fix."
5008
+ };
5009
+ }
5010
+ return {
5011
+ command: `context.add_source --project ${args.project_ref || "<project>"} --type local --path "${args.root_path}"`,
5012
+ message: "Attach the local workspace as a source, then rerun fix."
5013
+ };
5014
+ }
5015
+ async function attemptDeterministicSourceAttach(args) {
5016
+ const remote = parseGitHubRemote(getGitRemoteUrl(args.root_path) || null);
5017
+ if (remote) {
5018
+ const branch = getGitBranch(args.root_path) || "main";
5019
+ const result = await createSourceByType({
5020
+ project: args.project,
5021
+ type: "github",
5022
+ name: `auto-repair:${remote.owner}/${remote.repo}`,
5023
+ owner: remote.owner,
5024
+ repo: remote.repo,
5025
+ branch,
5026
+ auto_index: true
5027
+ });
5028
+ const sourceId = result?.source_id || result?.id;
5029
+ noteAutomaticSourceActivity({
5030
+ project: args.project,
5031
+ sourceIds: sourceId ? [String(sourceId)] : []
5032
+ });
5033
+ return { attached: true, detail: `github:${remote.owner}/${remote.repo}@${branch}` };
5034
+ }
5035
+ if (RUNTIME_MODE !== "remote") {
5036
+ await createSourceByType({
5037
+ project: args.project,
5038
+ type: "local",
5039
+ name: "auto-repair:local-workspace",
5040
+ path: args.root_path,
5041
+ auto_index: true,
5042
+ max_files: 500
5043
+ });
5044
+ return { attached: true, detail: "local_workspace_source_attached" };
5045
+ }
5046
+ return { attached: false, detail: "deterministic_source_inputs_unavailable" };
5047
+ }
5048
+ async function runAutoRepairVerification(args) {
5049
+ if (args.preflight.retrieval_route === "project_repo" && args.preflight.matched_source_ids.length > 0) {
5050
+ try {
5051
+ const queryResult = await queryWithDegradedFallback({
5052
+ project: args.project,
5053
+ query: "how does this project work",
5054
+ top_k: 5,
5055
+ include_memories: false,
5056
+ include_graph: false,
5057
+ source_ids: args.preflight.matched_source_ids
5058
+ });
5059
+ const resultCount = Array.isArray(queryResult.response?.results) ? queryResult.response.results.length : 0;
5060
+ return {
5061
+ passed: resultCount > 0,
5062
+ route: "project_repo",
5063
+ result_count: resultCount,
5064
+ detail: resultCount > 0 ? "project_query_has_results" : "project_query_empty"
5065
+ };
5066
+ } catch (error) {
5067
+ return {
5068
+ passed: false,
5069
+ route: "project_repo",
5070
+ result_count: 0,
5071
+ detail: `project_query_error:${String(error?.message || error)}`
5072
+ };
5073
+ }
5074
+ }
5075
+ if (args.preflight.retrieval_route === "local_workspace_fallback") {
5076
+ try {
5077
+ const local = await runLocalWorkspaceRetrieval({
5078
+ query: "how does this project work",
5079
+ path: args.preflight.trust_state.root_path,
5080
+ project: args.project,
5081
+ top_k: 5
5082
+ });
5083
+ return {
5084
+ passed: local.results.length > 0,
5085
+ route: "local_workspace_fallback",
5086
+ result_count: local.results.length,
5087
+ detail: local.results.length > 0 ? "local_query_has_results" : "local_query_empty"
5088
+ };
5089
+ } catch (error) {
5090
+ return {
5091
+ passed: false,
5092
+ route: "local_workspace_fallback",
5093
+ result_count: 0,
5094
+ detail: `local_query_error:${String(error?.message || error)}`
5095
+ };
5096
+ }
5097
+ }
5098
+ return {
5099
+ passed: false,
5100
+ route: args.preflight.retrieval_route,
5101
+ result_count: 0,
5102
+ detail: "no_verifiable_retrieval_route"
5103
+ };
5104
+ }
5105
+ async function runAutoRepairFlow(input) {
5106
+ const actions_attempted = [];
5107
+ const actions_succeeded = [];
5108
+ const actions_failed = [];
5109
+ const apply = input.apply !== false;
5110
+ const repairScope = input.repair_scope || "safe";
5111
+ const allowRestartHint = input.allow_restart_hint !== false;
5112
+ let restartRequired = false;
5113
+ if (!API_KEY) {
5114
+ return {
5115
+ root_cause_code: "missing_api_key",
5116
+ actions_attempted,
5117
+ actions_succeeded,
5118
+ actions_failed: [...actions_failed, "validate_credentials:missing_api_key"],
5119
+ restart_required: false,
5120
+ verification_passed: false,
5121
+ status_label: "blocked",
5122
+ status_action: "reauth",
5123
+ action_required: true,
5124
+ diagnostics: {
5125
+ message: "WHISPER_API_KEY is required."
5126
+ }
5127
+ };
5128
+ }
5129
+ actions_attempted.push("validate_credentials");
5130
+ actions_succeeded.push("validate_credentials");
5131
+ actions_attempted.push("resolve_project");
5132
+ const resolvedProject = await resolveProjectRef(input.project);
5133
+ if (!resolvedProject) {
5134
+ actions_failed.push("resolve_project:no_project");
5135
+ return {
5136
+ root_cause_code: "no_project_bound",
5137
+ actions_attempted,
5138
+ actions_succeeded,
5139
+ actions_failed,
5140
+ restart_required: false,
5141
+ verification_passed: false,
5142
+ status_label: "needs_user_action",
5143
+ status_action: "add_source",
5144
+ action_required: true,
5145
+ diagnostics: {
5146
+ message: "No project resolved. Set WHISPER_PROJECT or pass project."
5147
+ }
5148
+ };
5149
+ }
5150
+ actions_succeeded.push("resolve_project");
5151
+ actions_attempted.push("resolve_workspace_trust");
5152
+ const beforePreflight = await resolveRepoGroundingPreflight({
5153
+ query: "how does this project work",
5154
+ path: input.path,
5155
+ workspace_id: input.workspace_id,
5156
+ project: resolvedProject
5157
+ });
5158
+ actions_succeeded.push("resolve_workspace_trust");
5159
+ actions_attempted.push("runtime_mode_check");
5160
+ if (allowRestartHint && RUNTIME_MODE === "remote" && (beforePreflight.project_retrieval_readiness === "project_bound_no_repo_source" || beforePreflight.retrieval_readiness === "local_fallback")) {
5161
+ actions_failed.push("runtime_mode_check:remote_mode_blocks_local_repair");
5162
+ restartRequired = true;
5163
+ } else {
5164
+ actions_succeeded.push("runtime_mode_check");
5165
+ }
5166
+ actions_attempted.push("refresh_workspace_index_metadata");
5167
+ let refreshResult = null;
5168
+ try {
5169
+ if (apply) {
5170
+ refreshResult = await runBackgroundWorkspaceRefresh({
5171
+ workspace_id: beforePreflight.trust_state.workspace_id,
5172
+ root_path: beforePreflight.trust_state.root_path,
5173
+ force_full: repairScope === "full"
5174
+ });
5175
+ } else {
5176
+ refreshResult = { refreshed: false, mode: null, reason: "dry_run" };
5177
+ }
5178
+ actions_succeeded.push("refresh_workspace_index_metadata");
5179
+ } catch (error) {
5180
+ actions_failed.push(`refresh_workspace_index_metadata:${String(error?.message || error)}`);
5181
+ }
5182
+ actions_attempted.push("repair_source_readiness");
5183
+ let sourceRepair = {
5184
+ applied: false,
5185
+ attached: false,
5186
+ detail: "not_required",
5187
+ hint: null
5188
+ };
5189
+ if (beforePreflight.project_retrieval_readiness === "project_bound_no_repo_source") {
5190
+ const hint = buildSourceRepairHint({
5191
+ root_path: beforePreflight.trust_state.root_path,
5192
+ project_ref: beforePreflight.trust_state.project_ref
5193
+ });
5194
+ sourceRepair = {
5195
+ applied: apply && repairScope === "full",
5196
+ attached: false,
5197
+ detail: "manual_source_attach_required",
5198
+ hint
5199
+ };
5200
+ if (apply && repairScope === "full") {
5201
+ try {
5202
+ const attach = await attemptDeterministicSourceAttach({
5203
+ project: resolvedProject,
5204
+ root_path: beforePreflight.trust_state.root_path
5205
+ });
5206
+ sourceRepair = {
5207
+ applied: true,
5208
+ attached: attach.attached,
5209
+ detail: attach.detail,
5210
+ hint
5211
+ };
5212
+ if (attach.attached) {
5213
+ actions_succeeded.push("repair_source_readiness");
5214
+ } else {
5215
+ actions_failed.push(`repair_source_readiness:${attach.detail}`);
5216
+ }
5217
+ } catch (error) {
5218
+ actions_failed.push(`repair_source_readiness:${String(error?.message || error)}`);
5219
+ }
5220
+ } else {
5221
+ actions_failed.push("repair_source_readiness:manual_source_attach_required");
5222
+ }
5223
+ } else {
5224
+ actions_succeeded.push("repair_source_readiness");
5225
+ }
5226
+ const afterPreflight = await resolveRepoGroundingPreflight({
5227
+ query: "how does this project work",
5228
+ path: beforePreflight.trust_state.root_path,
5229
+ workspace_id: beforePreflight.trust_state.workspace_id,
5230
+ project: resolvedProject
5231
+ });
5232
+ const semanticStats = getWorkspaceSemanticFailureStats(afterPreflight.trust_state.workspace_id);
5233
+ const trustScore = computeTrustScore({
5234
+ retrieval_readiness: afterPreflight.retrieval_readiness,
5235
+ workspace_health: afterPreflight.trust_state.health,
5236
+ semantic_failure_rate: semanticStats.rate
5237
+ });
5238
+ persistTrustScore(afterPreflight.trust_state.workspace_id, trustScore);
5239
+ const autoHeal = shouldAutoHeal({
5240
+ retrieval_readiness: afterPreflight.retrieval_readiness,
5241
+ trust_score: trustScore,
5242
+ semantic_failure_rate: semanticStats.rate,
5243
+ semantic_attempts: semanticStats.attempts
5244
+ });
5245
+ actions_attempted.push("verification_query");
5246
+ const verification = await runAutoRepairVerification({
5247
+ project: resolvedProject,
5248
+ preflight: afterPreflight
5249
+ });
5250
+ if (verification.passed) actions_succeeded.push("verification_query");
5251
+ else actions_failed.push(`verification_query:${verification.detail}`);
5252
+ const rootCause = resolveAutoRepairRootCause({
5253
+ restart_required: restartRequired,
5254
+ trust_health: beforePreflight.trust_state.health,
5255
+ project_readiness: beforePreflight.project_retrieval_readiness,
5256
+ retrieval_readiness: beforePreflight.retrieval_readiness,
5257
+ semantic_failure_rate: semanticStats.rate,
5258
+ semantic_attempts: semanticStats.attempts
5259
+ });
5260
+ const statusContract = statusContractForReadiness({
5261
+ retrieval_readiness: afterPreflight.retrieval_readiness,
5262
+ workspace_health: afterPreflight.trust_state.health,
5263
+ root_cause_code: rootCause,
5264
+ force_repairing: autoHeal && rootCause === "none"
5265
+ });
5266
+ const verificationPassed = verification.passed;
5267
+ const effectiveStatusAction = statusContract.status_action;
5268
+ const effectiveActionRequired = statusContract.action_required;
5269
+ const transitionImproved = isStrictlyBetterReadiness(
5270
+ beforePreflight.retrieval_readiness,
5271
+ afterPreflight.retrieval_readiness
5272
+ );
5273
+ const explicitBlocker = effectiveActionRequired;
5274
+ const recommendedNextCalls = sanitizeRecommendedNextCalls([
5275
+ ...recommendedNextCallsForRootCause(rootCause),
5276
+ ...afterPreflight.recommended_next_calls
5277
+ ]);
5278
+ return {
5279
+ root_cause_code: rootCause,
5280
+ actions_attempted,
5281
+ actions_succeeded,
5282
+ actions_failed,
5283
+ restart_required: restartRequired,
5284
+ verification_passed: verificationPassed,
5285
+ status_label: statusContract.status_label,
5286
+ status_action: effectiveStatusAction,
5287
+ action_required: effectiveActionRequired,
5288
+ recommended_next_calls: recommendedNextCalls,
5289
+ diagnostics: {
5290
+ runtime_mode: RUNTIME_MODE,
5291
+ workspace_id: afterPreflight.trust_state.workspace_id,
5292
+ root_path: afterPreflight.trust_state.root_path,
5293
+ project_ref: afterPreflight.trust_state.project_ref,
5294
+ retrieval_readiness_before: beforePreflight.retrieval_readiness,
5295
+ retrieval_readiness_after: afterPreflight.retrieval_readiness,
5296
+ project_retrieval_readiness_after: afterPreflight.project_retrieval_readiness,
5297
+ retrieval_readiness_label: retrievalReadinessLabel(afterPreflight.retrieval_readiness),
5298
+ retrieval_readiness_action: retrievalReadinessAction(afterPreflight.retrieval_readiness),
5299
+ trust_score: trustScore,
5300
+ semantic_failure_rate: semanticStats.rate,
5301
+ semantic_attempts: semanticStats.attempts,
5302
+ refresh: refreshResult,
5303
+ source_repair: sourceRepair,
5304
+ verification,
5305
+ auto_heal_triggered: autoHeal,
5306
+ transition_improved: transitionImproved,
5307
+ explicit_blocker: explicitBlocker,
5308
+ recommended_next_calls: recommendedNextCalls,
5309
+ restart_hint: restartRequired ? "Set WHISPER_MCP_MODE=auto in MCP config and restart client." : null
5310
+ }
5311
+ };
5312
+ }
4634
5313
  function toTextResult(payload) {
4635
5314
  return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
4636
5315
  }
@@ -4710,29 +5389,69 @@ async function searchMemoriesForContextQuery(args) {
4710
5389
  }
4711
5390
  async function runContextQueryMemoryRescue(args) {
4712
5391
  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({
5392
+ const personalResults = formatCanonicalMemoryResults(scoped);
5393
+ const sharedUserId = sharedProjectMemoryUserId(args.project);
5394
+ const shared = args.user_id === sharedUserId ? { results: [] } : await searchMemoriesForContextQuery({
4721
5395
  project: args.project,
4722
5396
  query: args.query,
5397
+ user_id: sharedUserId,
4723
5398
  top_k: args.top_k
4724
5399
  });
4725
- return { results: formatCanonicalMemoryResults(broad), rescue_mode: formatCanonicalMemoryResults(broad).length > 0 ? "project_broad" : null };
5400
+ const sharedResults = formatCanonicalMemoryResults(shared);
5401
+ const deduped = /* @__PURE__ */ new Set();
5402
+ const combined = [
5403
+ ...personalResults.map((result) => ({
5404
+ ...result,
5405
+ memory_scope: "personal",
5406
+ ranking_score: clamp012(Number(result.similarity ?? 0.5) + 0.08)
5407
+ })),
5408
+ ...sharedResults.map((result) => ({
5409
+ ...result,
5410
+ memory_scope: "shared_project",
5411
+ ranking_score: clamp012(Number(result.similarity ?? 0.5))
5412
+ }))
5413
+ ].filter((result) => {
5414
+ if (!result.id) return false;
5415
+ if (deduped.has(result.id)) return false;
5416
+ deduped.add(result.id);
5417
+ return true;
5418
+ }).sort((a, b) => b.ranking_score - a.ranking_score);
5419
+ const rescueMode = combined.length === 0 ? null : sharedResults.length > 0 ? "scoped_plus_shared" : "scoped_only";
5420
+ return {
5421
+ personal_results: personalResults,
5422
+ shared_results: sharedResults,
5423
+ combined_results: combined,
5424
+ rescue_mode: rescueMode
5425
+ };
4726
5426
  }
4727
5427
  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}`
5428
+ const lines = args.combined_results.map(
5429
+ (result, index) => `${index + 1}. [${result.memory_scope}, ${result.memory_type || "memory"}, score: ${result.ranking_score.toFixed(2)}] ${result.content}`
4730
5430
  );
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}):
5431
+ return `Found ${args.combined_results.length} memory result(s) (project=${args.project}, user=${args.scope.userId}, session=${args.scope.sessionId}, personal=${args.personal_results.length}, shared=${args.shared_results.length}, ordering=personal_then_shared_bias):
4733
5432
 
4734
5433
  ${lines.join("\n\n")}`;
4735
5434
  }
5435
+ function memoryRescueResultsToEvidence(args) {
5436
+ return args.hits.map(
5437
+ (hit) => toEvidenceRef({
5438
+ id: `memory:${hit.id || randomUUID()}`,
5439
+ source: "memory",
5440
+ document: "memory",
5441
+ content: hit.content,
5442
+ score: hit.ranking_score,
5443
+ retrieval_source: "memory",
5444
+ metadata: {
5445
+ path: `memory://${hit.memory_scope}/${hit.id || "unknown"}`,
5446
+ line_start: 1,
5447
+ line_end: 1,
5448
+ score: hit.ranking_score,
5449
+ memory_scope: hit.memory_scope,
5450
+ memory_type: hit.memory_type || "factual"
5451
+ }
5452
+ }, args.workspaceId, "memory")
5453
+ );
5454
+ }
4736
5455
  function likelyEmbeddingFailure(error) {
4737
5456
  const message = String(error?.message || error || "").toLowerCase();
4738
5457
  return message.includes("embedding") || message.includes("vector") || message.includes("timeout") || message.includes("timed out") || message.includes("temporarily unavailable");
@@ -4753,7 +5472,13 @@ async function queryWithDegradedFallback(params) {
4753
5472
  hybrid: true,
4754
5473
  rerank: true
4755
5474
  });
4756
- return { response, degraded_mode: false };
5475
+ return {
5476
+ response,
5477
+ degraded_mode: false,
5478
+ semantic_status: "ok",
5479
+ fallback_mode: "none",
5480
+ recommended_fixes: []
5481
+ };
4757
5482
  } catch (error) {
4758
5483
  if (!likelyEmbeddingFailure(error)) throw error;
4759
5484
  const response = await whisper.query({
@@ -4776,7 +5501,13 @@ async function queryWithDegradedFallback(params) {
4776
5501
  response,
4777
5502
  degraded_mode: true,
4778
5503
  degraded_reason: "Embedding/graph path unavailable; lexical fallback used.",
4779
- recommendation: "Check embedding service health, then re-run for full hybrid quality."
5504
+ recommendation: "Check embedding service health, then re-run for full hybrid quality.",
5505
+ semantic_status: "failed",
5506
+ fallback_mode: "lexical_backend",
5507
+ recommended_fixes: [
5508
+ "Run fix to auto-repair retrieval health.",
5509
+ "If this persists, re-authenticate and reindex workspace sources."
5510
+ ]
4780
5511
  };
4781
5512
  }
4782
5513
  }
@@ -4888,6 +5619,47 @@ async function runSearchCodeSearch(args) {
4888
5619
  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
5620
  };
4890
5621
  }
5622
+ function retrievalReadinessLabel(readiness) {
5623
+ if (readiness === "project_repo_source_ready") return "Project retrieval is healthy.";
5624
+ if (readiness === "project_repo_source_stale") return "Project source exists but is not ready.";
5625
+ if (readiness === "project_bound_no_repo_source") return "No matching project source is connected.";
5626
+ if (readiness === "project_unverified") return "Project/source verification failed.";
5627
+ if (readiness === "local_fallback") return "Using local fallback retrieval.";
5628
+ if (readiness === "no_project") return "No project is bound to this workspace.";
5629
+ return "Retrieval readiness is unknown.";
5630
+ }
5631
+ function retrievalReadinessAction(readiness) {
5632
+ if (readiness === "project_repo_source_ready") return "none";
5633
+ if (readiness === "project_repo_source_stale") return "Run fix or index.workspace_run to refresh workspace trust.";
5634
+ if (readiness === "project_bound_no_repo_source") {
5635
+ return RUNTIME_MODE === "remote" ? "Connect a repo source with context.add_source, then run fix." : "Run index.local_scan_ingest or fix.";
5636
+ }
5637
+ if (readiness === "project_unverified") return "Re-authenticate your API key and run fix.";
5638
+ if (readiness === "local_fallback") return "Run fix to restore project-grounded retrieval.";
5639
+ if (readiness === "no_project") return "Provide --project/WHISPER_PROJECT and run fix.";
5640
+ return "Run fix.";
5641
+ }
5642
+ function buildRecommendedFixes(args) {
5643
+ const fixes = /* @__PURE__ */ new Set();
5644
+ if (args.semantic_status === "failed") {
5645
+ fixes.add("Run fix to auto-repair retrieval health.");
5646
+ }
5647
+ if (args.fallback_mode === "lexical_rescue" || args.fallback_mode === "lexical_backend") {
5648
+ fixes.add("Check embedding/vector service health and retry.");
5649
+ }
5650
+ if (args.retrieval_readiness === "project_bound_no_repo_source") {
5651
+ fixes.add(
5652
+ RUNTIME_MODE === "remote" ? "Add a matching source with context.add_source." : "Ingest local files with index.local_scan_ingest."
5653
+ );
5654
+ }
5655
+ if (args.retrieval_readiness === "project_unverified") {
5656
+ fixes.add("Re-authenticate (valid WHISPER_API_KEY) and rerun fix.");
5657
+ }
5658
+ if (args.retrieval_readiness === "project_repo_source_stale") {
5659
+ fixes.add("Refresh workspace metadata via index.workspace_run.");
5660
+ }
5661
+ return [...fixes];
5662
+ }
4891
5663
  function buildLocalWorkspaceDocuments(rootPath, allowedExts, maxFiles) {
4892
5664
  const files = collectCodeFiles(rootPath, allowedExts, maxFiles);
4893
5665
  const documents = [];
@@ -5023,20 +5795,6 @@ function localHitsToEvidence(workspaceId, hits) {
5023
5795
  )
5024
5796
  );
5025
5797
  }
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
5798
  async function runSearchCodeTool(args) {
5041
5799
  const rootPath = canonicalizeWorkspacePath(args.path);
5042
5800
  const allowedExts = args.file_types ? new Set(args.file_types) : CODE_EXTENSIONS;
@@ -5044,12 +5802,21 @@ async function runSearchCodeTool(args) {
5044
5802
  if (files.length === 0) {
5045
5803
  const workspace = await resolveWorkspaceTrust({ path: rootPath });
5046
5804
  const sharedWarnings = [...workspace.warnings];
5805
+ const semanticStatus = "ok";
5806
+ const fallbackMode = "none";
5047
5807
  return {
5048
5808
  tool: "search_code",
5049
5809
  query: args.query,
5050
5810
  path: rootPath,
5051
5811
  results: [],
5052
5812
  count: 0,
5813
+ semantic_status: semanticStatus,
5814
+ fallback_mode: fallbackMode,
5815
+ recommended_fixes: buildRecommendedFixes({
5816
+ retrieval_readiness: workspace.project_ref ? "project_repo_source_ready" : "no_project",
5817
+ semantic_status: semanticStatus,
5818
+ fallback_mode: fallbackMode
5819
+ }),
5053
5820
  warnings: sharedWarnings,
5054
5821
  diagnostics: {
5055
5822
  workspace_id: workspace.workspace_id,
@@ -5078,6 +5845,13 @@ async function runSearchCodeTool(args) {
5078
5845
  path: rootPath,
5079
5846
  results: localRetrieval.results,
5080
5847
  count: localRetrieval.results.length,
5848
+ semantic_status: semanticStatusFromSearchMode(localRetrieval.diagnostics.search_mode),
5849
+ fallback_mode: fallbackModeFromSearchMode(localRetrieval.diagnostics.search_mode),
5850
+ recommended_fixes: buildRecommendedFixes({
5851
+ retrieval_readiness: localRetrieval.workspace.project_ref ? "local_fallback" : "no_project",
5852
+ semantic_status: semanticStatusFromSearchMode(localRetrieval.diagnostics.search_mode),
5853
+ fallback_mode: fallbackModeFromSearchMode(localRetrieval.diagnostics.search_mode)
5854
+ }),
5081
5855
  warnings: localRetrieval.warnings,
5082
5856
  diagnostics: localRetrieval.diagnostics
5083
5857
  };
@@ -5331,7 +6105,8 @@ function renderScopedMcpConfig(project, source, client) {
5331
6105
  env: {
5332
6106
  WHISPER_API_KEY: "wctx_...",
5333
6107
  WHISPER_PROJECT: project,
5334
- WHISPER_SCOPE_SOURCE: source
6108
+ WHISPER_SCOPE_SOURCE: source,
6109
+ WHISPER_MCP_MODE: "auto"
5335
6110
  }
5336
6111
  };
5337
6112
  if (client === "json") {
@@ -5437,6 +6212,7 @@ server.tool(
5437
6212
  },
5438
6213
  async ({ workspace_id, path, mode, max_files }) => {
5439
6214
  try {
6215
+ const startAt = Date.now();
5440
6216
  const identity = resolveWorkspaceIdentity({ path, workspace_id });
5441
6217
  const rootPath = identity.root_path;
5442
6218
  const workspaceId = identity.workspace_id;
@@ -5445,11 +6221,17 @@ server.tool(
5445
6221
  const fileStats = countCodeFiles(rootPath, max_files);
5446
6222
  const coverage = Math.max(0, Math.min(1, fileStats.total / Math.max(1, max_files)));
5447
6223
  const now = (/* @__PURE__ */ new Date()).toISOString();
6224
+ const snapshot = computeWorkspaceSnapshot(rootPath);
5448
6225
  workspace.root_path = rootPath;
5449
6226
  workspace.index_metadata = {
5450
6227
  last_indexed_at: now,
5451
6228
  last_indexed_commit: getGitHead(rootPath),
5452
- coverage: mode === "full" ? 1 : coverage
6229
+ coverage: mode === "full" ? 1 : coverage,
6230
+ indexed_files: fileStats.total,
6231
+ last_auto_refresh_at: now,
6232
+ ...mode === "full" ? { last_full_refresh_at: now } : {},
6233
+ last_snapshot_hash: snapshot.hash,
6234
+ last_snapshot_entries: snapshot.entries
5453
6235
  };
5454
6236
  saveState(state);
5455
6237
  const payload = {
@@ -5458,7 +6240,7 @@ server.tool(
5458
6240
  mode,
5459
6241
  indexed_files: fileStats.total,
5460
6242
  skipped_files: fileStats.skipped,
5461
- duration_ms: 0,
6243
+ duration_ms: Date.now() - startAt,
5462
6244
  warnings: fileStats.total === 0 ? ["No code files discovered for indexing."] : [],
5463
6245
  index_metadata: workspace.index_metadata
5464
6246
  };
@@ -5486,24 +6268,325 @@ server.tool(
5486
6268
  }
5487
6269
  const result = await ingestLocalPath({
5488
6270
  project: resolvedProject,
5489
- path: path || process.cwd(),
5490
- glob,
5491
- max_files,
5492
- chunk_chars
6271
+ path: path || process.cwd(),
6272
+ glob,
6273
+ max_files,
6274
+ chunk_chars
6275
+ });
6276
+ return toTextResult({
6277
+ source_id: `local_${result.workspace_id}`,
6278
+ status: "ready",
6279
+ job_id: `local_ingest_${Date.now()}`,
6280
+ index_started: true,
6281
+ warnings: result.skipped.slice(0, 20),
6282
+ details: result
6283
+ });
6284
+ } catch (error) {
6285
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
6286
+ }
6287
+ }
6288
+ );
6289
+ server.tool(
6290
+ "index.auto_repair",
6291
+ "Diagnose and apply deterministic retrieval/setup repairs. This is the canonical zero-stress repair path.",
6292
+ {
6293
+ path: z.string().optional(),
6294
+ workspace_id: z.string().optional(),
6295
+ project: z.string().optional(),
6296
+ apply: z.boolean().optional().default(true),
6297
+ repair_scope: z.enum(["safe", "full"]).optional().default("safe"),
6298
+ allow_restart_hint: z.boolean().optional().default(true)
6299
+ },
6300
+ async ({ path, workspace_id, project, apply, repair_scope, allow_restart_hint }) => {
6301
+ try {
6302
+ const payload = await runAutoRepairFlow({
6303
+ path,
6304
+ workspace_id,
6305
+ project,
6306
+ apply,
6307
+ repair_scope,
6308
+ allow_restart_hint
6309
+ });
6310
+ return toTextResult(payload);
6311
+ } catch (error) {
6312
+ return primaryToolError(error.message, "auto_repair_failed");
6313
+ }
6314
+ }
6315
+ );
6316
+ server.tool(
6317
+ "fix",
6318
+ "Primary alias for zero-stress setup/retrieval repair. Delegates to index.auto_repair.",
6319
+ {
6320
+ path: z.string().optional(),
6321
+ workspace_id: z.string().optional(),
6322
+ project: z.string().optional(),
6323
+ apply: z.boolean().optional().default(true),
6324
+ repair_scope: z.enum(["safe", "full"]).optional().default("safe"),
6325
+ allow_restart_hint: z.boolean().optional().default(true)
6326
+ },
6327
+ async ({ path, workspace_id, project, apply, repair_scope, allow_restart_hint }) => {
6328
+ try {
6329
+ const payload = await runAutoRepairFlow({
6330
+ path,
6331
+ workspace_id,
6332
+ project,
6333
+ apply,
6334
+ repair_scope,
6335
+ allow_restart_hint
6336
+ });
6337
+ return toTextResult(payload);
6338
+ } catch (error) {
6339
+ return primaryToolError(error.message, "fix_failed");
6340
+ }
6341
+ }
6342
+ );
6343
+ async function runUnifiedContextRetrieval(params) {
6344
+ const preflight = await resolveRepoGroundingPreflight({
6345
+ query: params.question,
6346
+ path: params.path,
6347
+ workspace_id: params.workspace_id,
6348
+ project: params.project,
6349
+ chunk_types: params.chunk_types
6350
+ });
6351
+ const retrievalProfile = resolveMcpRetrievalProfile(params.retrieval_profile);
6352
+ const backgroundRefresh = await runBackgroundWorkspaceRefresh({
6353
+ workspace_id: preflight.trust_state.workspace_id,
6354
+ root_path: preflight.trust_state.root_path,
6355
+ force_full: false
6356
+ });
6357
+ if (shouldAbstainForCodebaseTrust({
6358
+ retrievalProfile,
6359
+ codebaseIntent: preflight.repo_grounded,
6360
+ preflight
6361
+ })) {
6362
+ const abstainPayload = buildCodebaseTrustAbstainPayload({
6363
+ query: params.question,
6364
+ preflight,
6365
+ retrievalProfile
6366
+ });
6367
+ return {
6368
+ ...abstainPayload,
6369
+ semantic_status: "failed",
6370
+ fallback_mode: "none",
6371
+ recommended_fixes: buildRecommendedFixes({
6372
+ retrieval_readiness: preflight.retrieval_readiness,
6373
+ semantic_status: "failed",
6374
+ fallback_mode: "none"
6375
+ }),
6376
+ status_label: "blocked",
6377
+ status_action: "reindex",
6378
+ action_required: true,
6379
+ diagnostics: {
6380
+ retrieval_profile: retrievalProfile,
6381
+ background_refresh: backgroundRefresh
6382
+ }
6383
+ };
6384
+ }
6385
+ const resolvedProject = preflight.trust_state.project_ref || await resolveProjectRef(params.project);
6386
+ const scope = resolveMcpScope({
6387
+ project: resolvedProject || params.project,
6388
+ user_id: params.user_id,
6389
+ session_id: params.session_id,
6390
+ path: preflight.trust_state.root_path
6391
+ });
6392
+ const topK = params.top_k ?? 12;
6393
+ let semanticStatus = "ok";
6394
+ let fallbackMode = "none";
6395
+ let contextText = "";
6396
+ let evidence = [];
6397
+ let warnings = [...preflight.warnings];
6398
+ let retrievalReadiness = preflight.retrieval_readiness;
6399
+ let retrievalRoute = preflight.retrieval_route;
6400
+ let latencyMs = 0;
6401
+ let memoryContribution = null;
6402
+ if (preflight.retrieval_route === "local_workspace_fallback") {
6403
+ const localRetrieval = await runLocalWorkspaceRetrieval({
6404
+ query: params.question,
6405
+ path: preflight.trust_state.root_path,
6406
+ project: resolvedProject || params.project,
6407
+ top_k: topK
6408
+ });
6409
+ semanticStatus = semanticStatusFromSearchMode(localRetrieval.diagnostics.search_mode);
6410
+ fallbackMode = fallbackModeFromSearchMode(localRetrieval.diagnostics.search_mode);
6411
+ evidence = localHitsToEvidence(preflight.trust_state.workspace_id, localRetrieval.results);
6412
+ contextText = evidence.map((item) => `[${renderCitation(item)}] ${item.snippet || "Relevant context found."}`).join("\n");
6413
+ warnings = Array.from(/* @__PURE__ */ new Set([...warnings, ...localRetrieval.warnings]));
6414
+ retrievalReadiness = "local_fallback";
6415
+ retrievalRoute = "local_workspace_fallback";
6416
+ } else if (!resolvedProject) {
6417
+ contextText = "";
6418
+ evidence = [];
6419
+ retrievalRoute = "none";
6420
+ } else {
6421
+ const queryResult = await queryWithDegradedFallback({
6422
+ project: resolvedProject,
6423
+ query: params.question,
6424
+ top_k: topK,
6425
+ include_memories: preflight.repo_grounded ? false : params.include_memories,
6426
+ include_graph: params.include_graph,
6427
+ session_id: params.session_id,
6428
+ user_id: scope.userId,
6429
+ source_ids: preflight.repo_grounded ? preflight.matched_source_ids : void 0,
6430
+ retrieval_profile: retrievalProfile,
6431
+ include_parent_content: params.include_parent_content
6432
+ });
6433
+ latencyMs = queryResult.response.meta?.latency_ms || 0;
6434
+ semanticStatus = queryResult.semantic_status || "ok";
6435
+ fallbackMode = queryResult.fallback_mode || "none";
6436
+ const rawResults = preflight.repo_grounded ? filterProjectRepoResults(queryResult.response.results || [], preflight.matched_source_ids) : queryResult.response.results || [];
6437
+ if (preflight.repo_grounded && rawResults.length === 0) {
6438
+ const localRetrieval = await runLocalWorkspaceRetrieval({
6439
+ query: params.question,
6440
+ path: preflight.trust_state.root_path,
6441
+ project: resolvedProject,
6442
+ top_k: topK
6443
+ });
6444
+ semanticStatus = semanticStatusFromSearchMode(localRetrieval.diagnostics.search_mode);
6445
+ fallbackMode = fallbackModeFromSearchMode(localRetrieval.diagnostics.search_mode);
6446
+ evidence = localHitsToEvidence(preflight.trust_state.workspace_id, localRetrieval.results);
6447
+ contextText = evidence.map((item) => `[${renderCitation(item)}] ${item.snippet || "Relevant context found."}`).join("\n");
6448
+ warnings = Array.from(/* @__PURE__ */ new Set([...warnings, ...localRetrieval.warnings]));
6449
+ retrievalReadiness = "local_fallback";
6450
+ retrievalRoute = "local_workspace_fallback";
6451
+ } else {
6452
+ contextText = queryResult.response.context || "";
6453
+ evidence = rawResults.map((r) => toEvidenceRef(r, preflight.trust_state.workspace_id, "semantic"));
6454
+ if (queryResult.degraded_mode) {
6455
+ warnings.push(queryResult.degraded_reason || "Lexical backend fallback active.");
6456
+ }
6457
+ retrievalRoute = preflight.repo_grounded ? "project_repo" : "none";
6458
+ }
6459
+ }
6460
+ if (resolvedProject && params.include_memories === true && !preflight.repo_grounded) {
6461
+ try {
6462
+ memoryContribution = await runContextQueryMemoryRescue({
6463
+ project: resolvedProject,
6464
+ query: params.question,
6465
+ user_id: scope.userId,
6466
+ session_id: params.session_id,
6467
+ top_k: Math.max(4, Math.min(topK, 8))
5493
6468
  });
5494
- return toTextResult({
5495
- source_id: `local_${result.workspace_id}`,
5496
- status: "ready",
5497
- job_id: `local_ingest_${Date.now()}`,
5498
- index_started: true,
5499
- warnings: result.skipped.slice(0, 20),
5500
- details: result
6469
+ if (memoryContribution.combined_results.length > 0) {
6470
+ const memoryEvidence = memoryRescueResultsToEvidence({
6471
+ workspaceId: preflight.trust_state.workspace_id,
6472
+ hits: memoryContribution.combined_results
6473
+ });
6474
+ const existingSourceIds = new Set(evidence.map((item) => item.source_id));
6475
+ const dedupedMemoryEvidence = memoryEvidence.filter((item) => !existingSourceIds.has(item.source_id));
6476
+ evidence = [...evidence, ...dedupedMemoryEvidence];
6477
+ const memoryContext = renderContextQueryMemoryRescue({
6478
+ project: resolvedProject,
6479
+ scope,
6480
+ personal_results: memoryContribution.personal_results,
6481
+ shared_results: memoryContribution.shared_results,
6482
+ combined_results: memoryContribution.combined_results
6483
+ });
6484
+ contextText = contextText.trim().length > 0 ? `${contextText}
6485
+
6486
+ ${memoryContext}` : memoryContext;
6487
+ if (retrievalRoute === "none") retrievalRoute = "memory_only";
6488
+ }
6489
+ } catch (error) {
6490
+ warnings.push(`Scoped memory contribution failed: ${String(error?.message || error)}`);
6491
+ }
6492
+ }
6493
+ recordSemanticAttempt(preflight.trust_state.workspace_id, semanticStatus === "failed");
6494
+ const semanticStats = getWorkspaceSemanticFailureStats(preflight.trust_state.workspace_id);
6495
+ const trustScore = computeTrustScore({
6496
+ retrieval_readiness: retrievalReadiness,
6497
+ workspace_health: preflight.trust_state.health,
6498
+ semantic_failure_rate: semanticStats.rate
6499
+ });
6500
+ persistTrustScore(preflight.trust_state.workspace_id, trustScore);
6501
+ const autoHeal = shouldAutoHeal({
6502
+ retrieval_readiness: retrievalReadiness,
6503
+ trust_score: trustScore,
6504
+ semantic_failure_rate: semanticStats.rate,
6505
+ semantic_attempts: semanticStats.attempts
6506
+ });
6507
+ let autoRepairSummary = null;
6508
+ if (autoHeal) {
6509
+ try {
6510
+ autoRepairSummary = await runAutoRepairFlow({
6511
+ path: preflight.trust_state.root_path,
6512
+ workspace_id: preflight.trust_state.workspace_id,
6513
+ project: resolvedProject || params.project,
6514
+ apply: true,
6515
+ repair_scope: "safe",
6516
+ allow_restart_hint: true
5501
6517
  });
6518
+ warnings.push("Auto-repair executed in safe mode.");
5502
6519
  } catch (error) {
5503
- return { content: [{ type: "text", text: `Error: ${error.message}` }] };
6520
+ warnings.push(`Auto-repair failed: ${String(error?.message || error)}`);
5504
6521
  }
5505
6522
  }
5506
- );
6523
+ const statusContract = statusContractForReadiness({
6524
+ retrieval_readiness: retrievalReadiness,
6525
+ workspace_health: preflight.trust_state.health,
6526
+ semantic_status: semanticStatus,
6527
+ force_repairing: autoHeal
6528
+ });
6529
+ const recommendedFixes = buildRecommendedFixes({
6530
+ retrieval_readiness: retrievalReadiness,
6531
+ semantic_status: semanticStatus,
6532
+ fallback_mode: fallbackMode
6533
+ });
6534
+ const recommendedNextCalls = sanitizeRecommendedNextCalls([
6535
+ ...preflight.recommended_next_calls,
6536
+ ...semanticStatus === "failed" ? ["fix"] : []
6537
+ ]);
6538
+ const payload = {
6539
+ question: params.question,
6540
+ workspace_id: preflight.trust_state.workspace_id,
6541
+ root_path: preflight.trust_state.root_path,
6542
+ trust_state: preflight.trust_state,
6543
+ retrieval_readiness: retrievalReadiness,
6544
+ retrieval_readiness_label: retrievalReadinessLabel(retrievalReadiness),
6545
+ retrieval_readiness_action: retrievalReadinessAction(retrievalReadiness),
6546
+ retrieval_route: retrievalRoute,
6547
+ context: contextText,
6548
+ evidence,
6549
+ total_results: evidence.length,
6550
+ latency_ms: latencyMs,
6551
+ semantic_status: semanticStatus,
6552
+ fallback_mode: fallbackMode,
6553
+ recommended_fixes: recommendedFixes,
6554
+ status_label: statusContract.status_label,
6555
+ status_action: statusContract.status_action,
6556
+ action_required: statusContract.action_required,
6557
+ warnings: Array.from(new Set(warnings)),
6558
+ recommended_next_calls: recommendedNextCalls,
6559
+ diagnostics: {
6560
+ scope: {
6561
+ project: resolvedProject || params.project || null,
6562
+ user_id: scope.userId,
6563
+ session_id: params.session_id || null
6564
+ },
6565
+ memory: {
6566
+ included: resolvedProject ? params.include_memories === true && !preflight.repo_grounded : false,
6567
+ personal_count: memoryContribution?.personal_results.length || 0,
6568
+ shared_count: memoryContribution?.shared_results.length || 0,
6569
+ combined_count: memoryContribution?.combined_results.length || 0,
6570
+ shared_user_id: resolvedProject ? sharedProjectMemoryUserId(resolvedProject) : null,
6571
+ ordering: "personal_then_shared_bias"
6572
+ },
6573
+ retrieval_profile: retrievalProfile,
6574
+ trust_score: trustScore,
6575
+ semantic_failure_rate: semanticStats.rate,
6576
+ auto_heal_triggered: autoHeal,
6577
+ auto_repair: autoRepairSummary,
6578
+ background_refresh: backgroundRefresh
6579
+ }
6580
+ };
6581
+ if (params.include_alias_warning) {
6582
+ payload.warnings = Array.from(/* @__PURE__ */ new Set([
6583
+ ...payload.warnings,
6584
+ "deprecated_alias_use_context_query",
6585
+ "deprecated_alias_use_context.query"
6586
+ ]));
6587
+ }
6588
+ return payload;
6589
+ }
5507
6590
  server.tool(
5508
6591
  "context.get_relevant",
5509
6592
  "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 +6605,23 @@ server.tool(
5522
6605
  },
5523
6606
  async ({ question, path, workspace_id, project, top_k, include_memories, include_graph, session_id, user_id, include_parent_content, retrieval_profile }) => {
5524
6607
  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,
6608
+ const payload = await runUnifiedContextRetrieval({
6609
+ question,
6610
+ path,
6611
+ workspace_id,
6612
+ project,
5588
6613
  top_k,
5589
- include_memories: preflight.repo_grounded ? false : include_memories,
6614
+ include_memories,
5590
6615
  include_graph,
5591
6616
  session_id,
5592
6617
  user_id,
5593
- source_ids: preflight.repo_grounded ? preflight.matched_source_ids : void 0,
5594
- retrieval_profile: retrievalProfile,
5595
- include_parent_content
6618
+ include_parent_content,
6619
+ retrieval_profile,
6620
+ include_alias_warning: true
5596
6621
  });
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) }] };
6622
+ return toTextResult(payload);
5648
6623
  } catch (error) {
5649
- return { content: [{ type: "text", text: `Error: ${error.message}` }] };
6624
+ return primaryToolError(error.message, "context_get_relevant_failed");
5650
6625
  }
5651
6626
  }
5652
6627
  );
@@ -5870,273 +6845,22 @@ server.tool(
5870
6845
  },
5871
6846
  async ({ project, query, path, top_k, chunk_types, include_memories, include_graph, user_id, session_id, max_tokens, include_parent_content, retrieval_profile }) => {
5872
6847
  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,
6848
+ const payload = await runUnifiedContextRetrieval({
6849
+ question: query,
6850
+ path,
6851
+ project,
6097
6852
  top_k,
6098
- include_memories: include_memories === true,
6853
+ include_memories,
6099
6854
  include_graph,
6100
- user_id: user_id || scope.userId,
6101
- session_id: session_id || scope.sessionId,
6102
- retrieval_profile: retrievalProfile,
6103
- include_parent_content
6855
+ user_id,
6856
+ session_id,
6857
+ include_parent_content,
6858
+ retrieval_profile,
6859
+ chunk_types
6104
6860
  });
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 }] };
6861
+ return toTextResult(payload);
6138
6862
  } catch (error) {
6139
- return { content: [{ type: "text", text: `Error: ${error.message}` }] };
6863
+ return primaryToolError(error.message, "context_query_failed");
6140
6864
  }
6141
6865
  }
6142
6866
  );
@@ -6147,32 +6871,54 @@ server.tool(
6147
6871
  project: z.string().optional().describe("Project name or slug"),
6148
6872
  content: z.string().describe("The memory content to store"),
6149
6873
  memory_type: z.enum(["factual", "preference", "event", "relationship", "opinion", "goal", "instruction"]).optional().default("factual"),
6874
+ scope: z.enum(["personal", "shared_project"]).optional().default("personal"),
6150
6875
  user_id: z.string().optional().describe("User this memory belongs to"),
6151
6876
  session_id: z.string().optional().describe("Session scope"),
6152
6877
  agent_id: z.string().optional().describe("Agent scope"),
6153
6878
  importance: z.number().optional().default(0.5).describe("Importance 0-1")
6154
6879
  },
6155
- async ({ project, content, memory_type, user_id, session_id, agent_id, importance }) => {
6880
+ async ({ project, content, memory_type, scope, user_id, session_id, agent_id, importance }) => {
6156
6881
  try {
6157
- const scope = resolveMcpScope({ project, user_id, session_id });
6882
+ const memoryScope = resolveMemoryScope({ project, user_id, session_id, scope });
6158
6883
  const result = await whisper.addMemory({
6159
- project: scope.project,
6884
+ project: memoryScope.project,
6160
6885
  content,
6161
6886
  memory_type,
6162
- user_id: scope.userId,
6163
- session_id: scope.sessionId,
6887
+ user_id: memoryScope.userId,
6888
+ session_id: memoryScope.sessionId,
6164
6889
  agent_id,
6165
- importance
6890
+ importance,
6891
+ metadata: buildMemoryScopeMetadata({
6892
+ memoryScope,
6893
+ agent_id
6894
+ })
6166
6895
  });
6167
6896
  const memoryId = result?.memory_id || result.id;
6168
6897
  const jobId = result?.job_id;
6169
6898
  const mode = result?.mode;
6170
6899
  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 }] };
6900
+ return primaryToolSuccess({
6901
+ tool: "memory.add",
6902
+ memory_id: memoryId || null,
6903
+ job_id: jobId || null,
6904
+ mode: mode || null,
6905
+ semantic_status: semanticStatus || null,
6906
+ memory_type: memory_type || "factual",
6907
+ scope: memoryScope.scopeMode,
6908
+ queued: mode === "async" || Boolean(jobId),
6909
+ diagnostics: {
6910
+ scope: {
6911
+ project: memoryScope.project || null,
6912
+ user_id: memoryScope.userId,
6913
+ session_id: memoryScope.sessionId || null,
6914
+ scope: memoryScope.scopeMode,
6915
+ actor_user_id: memoryScope.actorUserId,
6916
+ actor_session_id: memoryScope.actorSessionId || null
6917
+ }
6918
+ }
6919
+ });
6174
6920
  } catch (error) {
6175
- return { content: [{ type: "text", text: `Error: ${error.message}` }] };
6921
+ return primaryToolError(error.message);
6176
6922
  }
6177
6923
  }
6178
6924
  );
@@ -6182,28 +6928,29 @@ server.tool(
6182
6928
  {
6183
6929
  project: z.string().optional().describe("Project name or slug"),
6184
6930
  query: z.string().describe("What to search for"),
6931
+ scope: z.enum(["personal", "shared_project"]).optional().default("personal"),
6185
6932
  user_id: z.string().optional().describe("Filter by user"),
6186
6933
  session_id: z.string().optional().describe("Filter by session"),
6187
6934
  top_k: z.number().optional().default(10).describe("Number of results"),
6188
6935
  memory_types: z.array(z.enum(["factual", "preference", "event", "relationship", "opinion", "goal", "instruction"])).optional()
6189
6936
  },
6190
- async ({ project, query, user_id, session_id, top_k, memory_types }) => {
6937
+ async ({ project, query, scope, user_id, session_id, top_k, memory_types }) => {
6191
6938
  try {
6192
- const scope = resolveMcpScope({ project, user_id, session_id });
6939
+ const memoryScope = resolveMemoryScope({ project, user_id, session_id, scope });
6193
6940
  const results = runtimeClient && (!memory_types || memory_types.length <= 1) ? await runtimeClient.memory.search({
6194
- project: scope.project,
6941
+ project: memoryScope.project,
6195
6942
  query,
6196
- user_id: scope.userId,
6197
- session_id: scope.sessionId,
6943
+ user_id: memoryScope.userId,
6944
+ session_id: memoryScope.sessionId,
6198
6945
  top_k,
6199
6946
  include_pending: true,
6200
6947
  profile: "balanced",
6201
6948
  ...memory_types?.length === 1 ? { memory_type: memory_types[0] } : {}
6202
6949
  }) : await whisper.searchMemoriesSOTA({
6203
- project: scope.project,
6950
+ project: memoryScope.project,
6204
6951
  query,
6205
- user_id: scope.userId,
6206
- session_id: scope.sessionId,
6952
+ user_id: memoryScope.userId,
6953
+ session_id: memoryScope.sessionId,
6207
6954
  top_k,
6208
6955
  memory_types
6209
6956
  });
@@ -6211,10 +6958,19 @@ server.tool(
6211
6958
  return primaryToolSuccess({
6212
6959
  tool: "memory.search",
6213
6960
  query,
6214
- user_id: scope.userId,
6215
- session_id: scope.sessionId,
6961
+ scope: memoryScope.scopeMode,
6962
+ user_id: memoryScope.userId,
6963
+ session_id: memoryScope.sessionId,
6216
6964
  results: normalizedResults,
6217
- count: normalizedResults.length
6965
+ count: normalizedResults.length,
6966
+ diagnostics: {
6967
+ scope: {
6968
+ project: memoryScope.project || null,
6969
+ user_id: memoryScope.userId,
6970
+ session_id: memoryScope.sessionId || null,
6971
+ scope: memoryScope.scopeMode
6972
+ }
6973
+ }
6218
6974
  });
6219
6975
  } catch (error) {
6220
6976
  return primaryToolError(error.message);
@@ -6469,6 +7225,7 @@ server.tool(
6469
7225
  {
6470
7226
  project: z.string().optional().describe("Project name or slug"),
6471
7227
  query: z.string().describe("Search query (supports temporal: 'yesterday', 'last week')"),
7228
+ scope: z.enum(["personal", "shared_project"]).optional().default("personal"),
6472
7229
  user_id: z.string().optional().describe("Filter by user"),
6473
7230
  session_id: z.string().optional().describe("Filter by session"),
6474
7231
  question_date: z.string().optional().describe("ISO datetime for temporal grounding"),
@@ -6476,13 +7233,14 @@ server.tool(
6476
7233
  top_k: z.number().optional().default(10),
6477
7234
  include_relations: z.boolean().optional().default(true).describe("Include related memories via knowledge graph")
6478
7235
  },
6479
- async ({ project, query, user_id, session_id, question_date, memory_types, top_k, include_relations }) => {
7236
+ async ({ project, query, scope, user_id, session_id, question_date, memory_types, top_k, include_relations }) => {
6480
7237
  try {
7238
+ const memoryScope = resolveMemoryScope({ project, user_id, session_id, scope });
6481
7239
  const results = await whisper.searchMemoriesSOTA({
6482
- project,
7240
+ project: memoryScope.project,
6483
7241
  query,
6484
- user_id,
6485
- session_id,
7242
+ user_id: memoryScope.userId,
7243
+ session_id: memoryScope.sessionId,
6486
7244
  question_date,
6487
7245
  memory_types,
6488
7246
  top_k,
@@ -6492,10 +7250,21 @@ server.tool(
6492
7250
  return primaryToolSuccess({
6493
7251
  tool: "memory.search_sota",
6494
7252
  query,
7253
+ scope: memoryScope.scopeMode,
7254
+ user_id: memoryScope.userId,
7255
+ session_id: memoryScope.sessionId || null,
6495
7256
  question_date: question_date || null,
6496
7257
  include_relations,
6497
7258
  results: normalizedResults,
6498
- count: normalizedResults.length
7259
+ count: normalizedResults.length,
7260
+ diagnostics: {
7261
+ scope: {
7262
+ project: memoryScope.project || null,
7263
+ user_id: memoryScope.userId,
7264
+ session_id: memoryScope.sessionId || null,
7265
+ scope: memoryScope.scopeMode
7266
+ }
7267
+ }
6499
7268
  });
6500
7269
  } catch (error) {
6501
7270
  return primaryToolError(error.message);
@@ -6517,29 +7286,40 @@ server.tool(
6517
7286
  },
6518
7287
  async ({ project, session_id, user_id, messages }) => {
6519
7288
  try {
6520
- const scope = resolveMcpScope({ project, user_id, session_id });
7289
+ const memoryScope = resolveMemoryScope({
7290
+ project,
7291
+ user_id,
7292
+ session_id,
7293
+ scope: "personal"
7294
+ });
6521
7295
  const normalizedMessages = messages.map((message) => ({
6522
7296
  role: message.role,
6523
7297
  content: message.content,
6524
7298
  timestamp: message.timestamp
6525
7299
  }));
6526
7300
  const result = await ingestSessionWithSyncFallback({
6527
- project: scope.project,
6528
- session_id: scope.sessionId,
6529
- user_id: scope.userId,
7301
+ project: memoryScope.project,
7302
+ session_id: memoryScope.sessionId,
7303
+ user_id: memoryScope.userId,
6530
7304
  messages: normalizedMessages
6531
7305
  });
6532
- return {
6533
- content: [{
6534
- type: "text",
6535
- text: `Processed ${normalizedMessages.length} messages:
6536
- - Scope ${scope.project || "auto-project"} / ${scope.userId} / ${scope.sessionId}
6537
- - Created ${result.memories_created} memories
6538
- - Detected ${result.relations_created} relations
6539
- - Updated ${result.memories_invalidated} outdated memories` + (result.errors && result.errors.length > 0 ? `
6540
- - Errors: ${result.errors.join(", ")}` : "")
6541
- }]
6542
- };
7306
+ return primaryToolSuccess({
7307
+ tool: "memory.ingest_conversation",
7308
+ messages_processed: normalizedMessages.length,
7309
+ memories_created: result.memories_created,
7310
+ relations_created: result.relations_created,
7311
+ memories_invalidated: result.memories_invalidated,
7312
+ errors: result.errors || [],
7313
+ scope: memoryScope.scopeMode,
7314
+ diagnostics: {
7315
+ scope: {
7316
+ project: memoryScope.project || null,
7317
+ user_id: memoryScope.userId,
7318
+ session_id: memoryScope.sessionId || null,
7319
+ scope: memoryScope.scopeMode
7320
+ }
7321
+ }
7322
+ });
6543
7323
  } catch (error) {
6544
7324
  return { content: [{ type: "text", text: `Error: ${error.message}` }] };
6545
7325
  }
@@ -6756,6 +7536,9 @@ server.tool(
6756
7536
  {
6757
7537
  workspace_id: z.string().optional(),
6758
7538
  project: z.string().optional(),
7539
+ scope: z.enum(["personal", "shared_project"]).optional().default("personal"),
7540
+ user_id: z.string().optional(),
7541
+ session_id: z.string().optional(),
6759
7542
  target: z.object({
6760
7543
  memory_id: z.string().optional(),
6761
7544
  query: z.string().optional()
@@ -6763,7 +7546,7 @@ server.tool(
6763
7546
  mode: z.enum(["delete", "invalidate"]).optional().default("invalidate"),
6764
7547
  reason: z.string().optional()
6765
7548
  },
6766
- async ({ workspace_id, project, target, mode, reason }) => {
7549
+ async ({ workspace_id, project, scope, user_id, session_id, target, mode, reason }) => {
6767
7550
  try {
6768
7551
  if (!target.memory_id && !target.query) {
6769
7552
  return { content: [{ type: "text", text: "Error: target.memory_id or target.query is required." }] };
@@ -6772,8 +7555,22 @@ server.tool(
6772
7555
  let queryResolution = null;
6773
7556
  const now = (/* @__PURE__ */ new Date()).toISOString();
6774
7557
  const actor = process.env.WHISPER_AGENT_ID || process.env.USERNAME || "api_key_principal";
6775
- const resolvedProject = await resolveProjectRef(project);
7558
+ const memoryScope = resolveMemoryScope({
7559
+ project,
7560
+ user_id,
7561
+ session_id,
7562
+ scope
7563
+ });
7564
+ const resolvedProject = await resolveProjectRef(memoryScope.project);
7565
+ async function assertScopedMemoryId(memoryId) {
7566
+ const memoryResult = await whisper.getMemory(memoryId);
7567
+ const memory = memoryResult?.memory || memoryResult;
7568
+ if (!isMemoryInScope({ memory, memoryScope })) {
7569
+ throw new Error(`memory_id ${memoryId} is outside the effective ${memoryScope.scopeMode} scope.`);
7570
+ }
7571
+ }
6776
7572
  async function applyToMemory(memoryId) {
7573
+ await assertScopedMemoryId(memoryId);
6777
7574
  if (mode === "delete") {
6778
7575
  await whisper.deleteMemory(memoryId);
6779
7576
  } else {
@@ -6788,7 +7585,10 @@ server.tool(
6788
7585
  project: resolvedProject,
6789
7586
  content: `Invalidated memory ${memoryId} at ${now}. Reason: ${reason || "No reason provided"}`,
6790
7587
  memory_type: "instruction",
6791
- importance: 1
7588
+ user_id: memoryScope.userId,
7589
+ session_id: memoryScope.sessionId,
7590
+ importance: 1,
7591
+ metadata: buildMemoryScopeMetadata({ memoryScope })
6792
7592
  });
6793
7593
  }
6794
7594
  }
@@ -6820,13 +7620,25 @@ server.tool(
6820
7620
  mode: audit2.mode,
6821
7621
  ...audit2.reason ? { reason: audit2.reason } : {}
6822
7622
  },
6823
- warning: "No project resolved for query-based forget. Nothing was changed."
7623
+ warning: "No project resolved for query-based forget. Nothing was changed.",
7624
+ diagnostics: {
7625
+ scope: {
7626
+ project: memoryScope.project || null,
7627
+ user_id: memoryScope.userId,
7628
+ session_id: memoryScope.sessionId || null,
7629
+ scope: memoryScope.scopeMode,
7630
+ actor_user_id: memoryScope.actorUserId,
7631
+ actor_session_id: memoryScope.actorSessionId || null
7632
+ }
7633
+ }
6824
7634
  };
6825
7635
  return { content: [{ type: "text", text: JSON.stringify(payload2, null, 2) }] };
6826
7636
  }
6827
7637
  const search = await whisper.searchMemoriesSOTA({
6828
7638
  project: resolvedProject,
6829
7639
  query: target.query || "",
7640
+ user_id: memoryScope.userId,
7641
+ session_id: memoryScope.sessionId,
6830
7642
  top_k: 25,
6831
7643
  include_relations: false,
6832
7644
  include_pending: true
@@ -6839,7 +7651,17 @@ server.tool(
6839
7651
  status: "completed",
6840
7652
  affected_ids: affectedIds,
6841
7653
  warning: resolved.warning || "Query did not resolve to a reliable memory match. No memories were changed.",
6842
- resolved_by: resolved.resolved_by
7654
+ resolved_by: resolved.resolved_by,
7655
+ diagnostics: {
7656
+ scope: {
7657
+ project: resolvedProject,
7658
+ user_id: memoryScope.userId,
7659
+ session_id: memoryScope.sessionId || null,
7660
+ scope: memoryScope.scopeMode,
7661
+ actor_user_id: memoryScope.actorUserId,
7662
+ actor_session_id: memoryScope.actorSessionId || null
7663
+ }
7664
+ }
6843
7665
  };
6844
7666
  return { content: [{ type: "text", text: JSON.stringify(payload2, null, 2) }] };
6845
7667
  }
@@ -6868,6 +7690,16 @@ server.tool(
6868
7690
  called_at: audit.called_at,
6869
7691
  mode: audit.mode,
6870
7692
  ...audit.reason ? { reason: audit.reason } : {}
7693
+ },
7694
+ diagnostics: {
7695
+ scope: {
7696
+ project: resolvedProject || null,
7697
+ user_id: memoryScope.userId,
7698
+ session_id: memoryScope.sessionId || null,
7699
+ scope: memoryScope.scopeMode,
7700
+ actor_user_id: memoryScope.actorUserId,
7701
+ actor_session_id: memoryScope.actorSessionId || null
7702
+ }
6871
7703
  }
6872
7704
  };
6873
7705
  return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
@@ -7531,7 +8363,10 @@ server.tool(
7531
8363
  context: response.context,
7532
8364
  results: response.results,
7533
8365
  degradedMode: queryResult.degraded_mode,
7534
- degradedReason: queryResult.degraded_reason
8366
+ degradedReason: queryResult.degraded_reason,
8367
+ semanticStatus: queryResult.semantic_status,
8368
+ fallbackMode: queryResult.fallback_mode,
8369
+ recommendedFixes: queryResult.recommended_fixes || []
7535
8370
  }));
7536
8371
  } catch (error) {
7537
8372
  return primaryToolError(error.message);
@@ -7788,14 +8623,28 @@ server.tool(
7788
8623
  project: z.string().optional(),
7789
8624
  content: z.string().describe("Memory content"),
7790
8625
  memory_type: z.enum(["factual", "preference", "event", "relationship", "opinion", "goal", "instruction"]).optional().default("factual"),
8626
+ scope: z.enum(["personal", "shared_project"]).optional().default("personal"),
7791
8627
  user_id: z.string().optional(),
7792
8628
  session_id: z.string().optional(),
7793
8629
  agent_id: z.string().optional(),
7794
8630
  importance: z.number().optional().default(0.5)
7795
8631
  },
7796
- async ({ project, content, memory_type, user_id, session_id, agent_id, importance }) => {
8632
+ async ({ project, content, memory_type, scope, user_id, session_id, agent_id, importance }) => {
7797
8633
  try {
7798
- const result = await whisper.addMemory({ project, content, memory_type, user_id, session_id, agent_id, importance });
8634
+ const memoryScope = resolveMemoryScope({ project, user_id, session_id, scope });
8635
+ const result = await whisper.addMemory({
8636
+ project: memoryScope.project,
8637
+ content,
8638
+ memory_type,
8639
+ user_id: memoryScope.userId,
8640
+ session_id: memoryScope.sessionId,
8641
+ agent_id,
8642
+ importance,
8643
+ metadata: buildMemoryScopeMetadata({
8644
+ memoryScope,
8645
+ agent_id
8646
+ })
8647
+ });
7799
8648
  const memoryId = result?.memory_id || (result.mode === "sync" ? result.id : null);
7800
8649
  const jobId = result?.job_id || (result.mode === "async" ? result.id : null);
7801
8650
  return primaryToolSuccess({
@@ -7805,8 +8654,19 @@ server.tool(
7805
8654
  job_id: jobId,
7806
8655
  mode: result?.mode || null,
7807
8656
  memory_type,
8657
+ scope: memoryScope.scopeMode,
7808
8658
  stored: result.success === true,
7809
- queued: result?.mode === "async" || Boolean(jobId)
8659
+ queued: result?.mode === "async" || Boolean(jobId),
8660
+ diagnostics: {
8661
+ scope: {
8662
+ project: memoryScope.project || null,
8663
+ user_id: memoryScope.userId,
8664
+ session_id: memoryScope.sessionId || null,
8665
+ scope: memoryScope.scopeMode,
8666
+ actor_user_id: memoryScope.actorUserId,
8667
+ actor_session_id: memoryScope.actorSessionId || null
8668
+ }
8669
+ }
7810
8670
  });
7811
8671
  } catch (error) {
7812
8672
  return primaryToolError(error.message);
@@ -7842,18 +8702,33 @@ server.tool(
7842
8702
  timestamp
7843
8703
  });
7844
8704
  const resolvedProject = await resolveProjectRef(project);
7845
- const result = await ingestSessionWithSyncFallback({
7846
- project: resolvedProject,
7847
- session_id,
8705
+ const memoryScope = resolveMemoryScope({
8706
+ project: resolvedProject || project,
7848
8707
  user_id,
8708
+ session_id,
8709
+ scope: "personal"
8710
+ });
8711
+ const result = await ingestSessionWithSyncFallback({
8712
+ project: memoryScope.project,
8713
+ session_id: memoryScope.sessionId || session_id,
8714
+ user_id: memoryScope.userId,
7849
8715
  messages: normalizedMessages
7850
8716
  });
7851
8717
  return primaryToolSuccess({
7852
8718
  tool: "record",
7853
- session_id,
8719
+ session_id: memoryScope.sessionId || session_id,
8720
+ user_id: memoryScope.userId,
7854
8721
  messages_recorded: normalizedMessages.length,
7855
8722
  memories_created: result.memories_created,
7856
- relations_created: result.relations_created
8723
+ relations_created: result.relations_created,
8724
+ diagnostics: {
8725
+ scope: {
8726
+ project: memoryScope.project || null,
8727
+ user_id: memoryScope.userId,
8728
+ session_id: memoryScope.sessionId || session_id,
8729
+ scope: memoryScope.scopeMode
8730
+ }
8731
+ }
7857
8732
  });
7858
8733
  } catch (error) {
7859
8734
  return primaryToolError(error.message);
@@ -7984,6 +8859,14 @@ async function main() {
7984
8859
  console.error("Error: WHISPER_API_KEY environment variable is required");
7985
8860
  process.exit(1);
7986
8861
  }
8862
+ const startupDiagnostics = {
8863
+ runtime_mode: RUNTIME_MODE,
8864
+ local_ingest_enabled: RUNTIME_MODE !== "remote",
8865
+ startup_status: RUNTIME_MODE === "remote" ? "degraded_auto_repairing" : "healthy",
8866
+ startup_action: RUNTIME_MODE === "remote" ? "restart_client" : "none",
8867
+ 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
8868
+ };
8869
+ console.error(`[whisper-context-mcp] startup_diagnostics=${JSON.stringify(startupDiagnostics)}`);
7987
8870
  console.error("[whisper-context-mcp] Breaking change: canonical namespaced tool names are active. Run with --print-tool-map for migration mapping.");
7988
8871
  const transport = new StdioServerTransport();
7989
8872
  await server.connect(transport);
@@ -7996,20 +8879,35 @@ export {
7996
8879
  RECOMMENDED_NEXT_CALL_ALLOWLIST,
7997
8880
  RETRIEVAL_READINESS_VALUES,
7998
8881
  RETRIEVAL_ROUTE_VALUES,
8882
+ STATUS_ACTION_VALUES,
8883
+ STATUS_LABEL_VALUES,
7999
8884
  WORKSPACE_HEALTH_VALUES,
8000
8885
  canonicalizeWorkspacePath,
8001
8886
  chooseWorkspaceProjectSource,
8002
8887
  classifyProjectRepoReadiness,
8003
8888
  classifyRepoGroundedQuery,
8004
8889
  classifyWorkspaceHealth,
8890
+ computeSnapshotChangedFiles,
8891
+ computeTrustScore,
8005
8892
  createMcpServer,
8006
8893
  createWhisperMcpClient,
8007
8894
  createWhisperMcpRuntimeClient,
8895
+ defaultMcpUserId,
8008
8896
  extractSignature,
8897
+ fallbackModeFromSearchMode,
8898
+ isStrictlyBetterReadiness,
8899
+ recommendedNextCallsAreAcyclic,
8900
+ recommendedNextCallsForRootCause,
8009
8901
  renderScopedMcpConfig,
8010
8902
  resolveForgetQueryCandidates,
8903
+ resolveMcpScope,
8011
8904
  resolveWorkspaceIdentity,
8905
+ retrievalReadinessRank,
8012
8906
  runSearchCodeSearch,
8013
8907
  sanitizeRecommendedNextCalls,
8014
- shouldAbstainForCodebaseTrust
8908
+ semanticStatusFromSearchMode,
8909
+ shouldAbstainForCodebaseTrust,
8910
+ shouldTriggerBackgroundRefresh,
8911
+ statusActionForRootCause,
8912
+ statusContractForReadiness
8015
8913
  };